@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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Restaurants online ordering — Wix's visitor "Place an order from a custom storefront" flow,
|
|
2
|
+
// shared by the dish detail (himi-branded-lite-menu-item: add a dish to the Wix cart) and the
|
|
3
|
+
// order screen (wix-spaces-food-orders: cart → pickup/delivery → hosted checkout → history).
|
|
4
|
+
//
|
|
5
|
+
// The cart is the site's eCom CURRENT cart (Cart V2). It is the same cart the Stores cart screen
|
|
6
|
+
// reads (V1 and V2 answer the same cart id), so a restaurant line is told apart by its catalog
|
|
7
|
+
// app — Restaurants Orders — never assumed. Every call here is visitor-scope; the member's placed
|
|
8
|
+
// orders are read through the eCom orders search the member Orders screen already uses, never
|
|
9
|
+
// through the owner-capable Restaurants Orders API.
|
|
10
|
+
import {
|
|
11
|
+
getFirstAvailableTimeSlotPerFulfillmentType,
|
|
12
|
+
listFulfillmentMethods,
|
|
13
|
+
listOperations,
|
|
14
|
+
queryMenus,
|
|
15
|
+
querySections,
|
|
16
|
+
type WixFulfillmentMethod,
|
|
17
|
+
type WixFulfillmentTimeSlot,
|
|
18
|
+
type WixMenu,
|
|
19
|
+
type WixOperation,
|
|
20
|
+
type WixRestaurantsAddress,
|
|
21
|
+
type WixSection,
|
|
22
|
+
} from "@himalaya/wix-restaurants";
|
|
23
|
+
import type { WixCartV2, WixCartV2LineItem, WixCatalogReference } from "@himalaya/wix-ecom";
|
|
24
|
+
import { formatCurrency } from "../_shared/engine-intl.js";
|
|
25
|
+
|
|
26
|
+
/** Wix Restaurants Orders — the catalog app a restaurant cart line belongs to. */
|
|
27
|
+
export const RESTAURANTS_ORDERS_APP_ID = "9a5d83fd-8570-482e-81ab-cfa88942ee60";
|
|
28
|
+
|
|
29
|
+
export type FulfillmentType = "PICKUP" | "DELIVERY";
|
|
30
|
+
|
|
31
|
+
/** The operation a visitor orders from: an `ENABLED` one, the site's default first. None means
|
|
32
|
+
* online ordering is closed (`DISABLED`, `PAUSED_UNTIL`, or no operation at all). */
|
|
33
|
+
export function orderingOperation(operations: WixOperation[] | undefined, selection: { businessLocationId?: string; operationId?: string } = {}): WixOperation | undefined {
|
|
34
|
+
const open = (operations ?? []).filter((operation) => !!operation?.id && operation.onlineOrderingStatus === "ENABLED");
|
|
35
|
+
if (selection.operationId) return open.find((operation) => operation.id === selection.operationId);
|
|
36
|
+
if (selection.businessLocationId) return open.find((operation) => operation.businessLocationId === selection.businessLocationId);
|
|
37
|
+
return open.find((operation) => operation.default === true) ?? open[0];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function loadOrderingOperation(selection: { businessLocationId?: string; operationId?: string } = {}): Promise<WixOperation | undefined> {
|
|
41
|
+
return orderingOperation((await listOperations()).operations, selection);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The enabled pickup/delivery methods [operation] offers: the ones its `fulfillmentIds` name. An
|
|
45
|
+
* operation that names none falls back only to methods at its own business location — never to
|
|
46
|
+
* every method on the site, which on a multi-location site would offer another location's pickup. */
|
|
47
|
+
export function offeredMethods(operation: WixOperation, methods: WixFulfillmentMethod[] | undefined): WixFulfillmentMethod[] {
|
|
48
|
+
const ids = new Set(operation.fulfillmentIds ?? []);
|
|
49
|
+
const belongs = (method: WixFulfillmentMethod) => ids.size > 0
|
|
50
|
+
? ids.has(method.id)
|
|
51
|
+
: !!operation.businessLocationId && method.businessLocationId === operation.businessLocationId;
|
|
52
|
+
return (methods ?? []).filter((method) =>
|
|
53
|
+
!!method?.id && method.enabled === true && (method.type === "PICKUP" || method.type === "DELIVERY") && belongs(method));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function loadOfferedMethods(operation: WixOperation): Promise<WixFulfillmentMethod[]> {
|
|
57
|
+
const all: WixFulfillmentMethod[] = [];
|
|
58
|
+
let cursor = "";
|
|
59
|
+
// A site has a handful of methods; five pages of 100 is a bound, not an expectation.
|
|
60
|
+
for (let page = 0; page < 5; page++) {
|
|
61
|
+
const response = await listFulfillmentMethods({ limit: 100, ...(cursor ? { cursor } : {}) });
|
|
62
|
+
all.push(...(response.fulfillmentMethods ?? []));
|
|
63
|
+
cursor = response.pagingMetadata?.hasNext ? response.pagingMetadata.cursors?.next ?? "" : "";
|
|
64
|
+
if (!cursor) break;
|
|
65
|
+
}
|
|
66
|
+
return offeredMethods(operation, all);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A decimal string from Wix ("6.99", "0") as a number; anything else is 0 (a missing fee is free). */
|
|
70
|
+
export function decimal(value: unknown): number {
|
|
71
|
+
const n = typeof value === "number" ? value : Number(typeof value === "string" ? value.trim() : NaN);
|
|
72
|
+
return Number.isFinite(n) ? n : 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Cart V2 money carries no formatted string: format the decimal with the cart's currency. */
|
|
76
|
+
export function money(amount: unknown, currency: string | undefined): string {
|
|
77
|
+
return formatCurrency(decimal(amount), (currency ?? "").trim() || "USD", "en");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function cartCurrency(cart: WixCartV2 | undefined): string {
|
|
81
|
+
return cart?.businessInfo?.currencyCode || cart?.customerInfo?.currencyCode || "";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Where a dish is ordered from. Checkout validates the line against `menuSectionId`, so it must
|
|
85
|
+
* be a visible section that lists the dish, inside a visible menu that lists the section. */
|
|
86
|
+
export interface DishPlacement {
|
|
87
|
+
menuId: string;
|
|
88
|
+
sectionId: string;
|
|
89
|
+
businessLocationId?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The section the dish was shown under wins when it qualifies; otherwise the first visible
|
|
93
|
+
* menu → section that lists it. None → the dish is on no orderable menu. */
|
|
94
|
+
export function pickPlacement(
|
|
95
|
+
dishId: string,
|
|
96
|
+
menus: WixMenu[],
|
|
97
|
+
sections: WixSection[],
|
|
98
|
+
hint: { menuId?: string; sectionId?: string } = {},
|
|
99
|
+
): DishPlacement | undefined {
|
|
100
|
+
const listing = new Map(sections
|
|
101
|
+
.filter((section) => !!section?.id && section.visible !== false && (section.itemIds ?? []).includes(dishId))
|
|
102
|
+
.map((section) => [section.id, section]));
|
|
103
|
+
const candidates: DishPlacement[] = [];
|
|
104
|
+
for (const menu of menus) {
|
|
105
|
+
if (!menu?.id || menu.visible === false) continue;
|
|
106
|
+
for (const sectionId of menu.sectionIds ?? []) {
|
|
107
|
+
if (listing.has(sectionId)) candidates.push({ menuId: menu.id, sectionId, businessLocationId: menu.businessLocationId });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return candidates.find((c) => c.sectionId === hint.sectionId && (!hint.menuId || c.menuId === hint.menuId)) ?? candidates[0];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function loadDishPlacement(dishId: string, hint: { menuId?: string; sectionId?: string }): Promise<DishPlacement | undefined> {
|
|
114
|
+
const [sections, menus] = await Promise.all([
|
|
115
|
+
querySections({ paging: { limit: 100 } }),
|
|
116
|
+
queryMenus({ paging: { limit: 50 } }),
|
|
117
|
+
]);
|
|
118
|
+
return pickPlacement(dishId, menus.menus ?? [], sections.sections ?? [], hint);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The catalog reference for one dish line. There is no `variantId` key in `options`. */
|
|
122
|
+
export function dishReference(dishId: string, operationId: string, placement: DishPlacement): WixCatalogReference {
|
|
123
|
+
return {
|
|
124
|
+
appId: RESTAURANTS_ORDERS_APP_ID,
|
|
125
|
+
catalogItemId: dishId,
|
|
126
|
+
options: {
|
|
127
|
+
operationId,
|
|
128
|
+
menuId: placement.menuId,
|
|
129
|
+
menuSectionId: placement.sectionId,
|
|
130
|
+
sectionId: placement.sectionId,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function isRestaurantLine(line: WixCartV2LineItem | undefined): boolean {
|
|
136
|
+
return line?.source?.catalogReference?.appId === RESTAURANTS_ORDERS_APP_ID;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function lineVariantId(line: WixCartV2LineItem): string {
|
|
140
|
+
const variant = (line.source?.catalogReference?.options as { priceVariant?: { id?: unknown } } | undefined)?.priceVariant;
|
|
141
|
+
return typeof variant?.id === "string" ? variant.id : "";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The cart line Wix kept for [reference] — same dish, operation, menu, section and variant — or
|
|
145
|
+
* undefined when Wix dropped it. Wix can refuse an item and still answer 200, so the returned cart
|
|
146
|
+
* is the proof. The operation and menu matter: the same dish added from two restaurant locations
|
|
147
|
+
* is two lines, and only the one just added proves this add. */
|
|
148
|
+
export function addedLine(cart: WixCartV2 | undefined, reference: WixCatalogReference): WixCartV2LineItem | undefined {
|
|
149
|
+
type PlacementOptions = { operationId?: unknown; menuId?: unknown; menuSectionId?: unknown };
|
|
150
|
+
const options = (reference.options ?? {}) as PlacementOptions & { priceVariant?: { id?: unknown } };
|
|
151
|
+
const variantId = typeof options.priceVariant?.id === "string" ? options.priceVariant.id : "";
|
|
152
|
+
return (cart?.lineItems ?? []).find((line) => {
|
|
153
|
+
const ref = line.source?.catalogReference;
|
|
154
|
+
if (!ref || ref.appId !== reference.appId || ref.catalogItemId !== reference.catalogItemId) return false;
|
|
155
|
+
const lineOptions = (ref.options ?? {}) as PlacementOptions;
|
|
156
|
+
if (lineOptions.operationId !== options.operationId || lineOptions.menuId !== options.menuId) return false;
|
|
157
|
+
if (lineOptions.menuSectionId !== options.menuSectionId) return false;
|
|
158
|
+
if (lineVariantId(line) !== variantId) return false;
|
|
159
|
+
if (line.status === "OUT_OF_STOCK" || line.status === "REMOVED_FROM_CATALOG") return false;
|
|
160
|
+
const confirmed = line.quantityInfo?.confirmedQuantity;
|
|
161
|
+
return confirmed === undefined || confirmed > 0;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function lineQuantity(line: WixCartV2LineItem): number {
|
|
166
|
+
const q = line.quantityInfo?.confirmedQuantity ?? line.quantityInfo?.requestedQuantity ?? 1;
|
|
167
|
+
return Number.isFinite(q) && q >= 0 ? Math.floor(q) : 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** How many dishes the cart holds (restaurant lines only). */
|
|
171
|
+
export function restaurantItemCount(cart: WixCartV2 | undefined): number {
|
|
172
|
+
return (cart?.lineItems ?? []).filter(isRestaurantLine).reduce((total, line) => total + lineQuantity(line), 0);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function slotFor(slots: WixFulfillmentTimeSlot[] | undefined, type: FulfillmentType): WixFulfillmentTimeSlot | undefined {
|
|
176
|
+
return (slots ?? []).find((slot) => slot?.fulfilmentType === type);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The `deliveryMethod.code` for [type]: `<TYPE>|ASAP`, or the slot's own window for a pre-order.
|
|
180
|
+
* An ASAP slot must be sent as ASAP — its start/end window is refused as `DELIVERY_METHOD_NOT_FOUND`. */
|
|
181
|
+
export function deliveryCode(type: FulfillmentType, slot?: WixFulfillmentTimeSlot): string {
|
|
182
|
+
const window = slot?.timeSlot;
|
|
183
|
+
if (window?.orderSchedulingType === "PREORDER") {
|
|
184
|
+
const start = Date.parse(window.startTime ?? ""), end = Date.parse(window.endTime ?? "");
|
|
185
|
+
if (Number.isFinite(start) && Number.isFinite(end)) return `${type}|${start}|${end}`;
|
|
186
|
+
}
|
|
187
|
+
return `${type}|ASAP`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function firstSlots(operationId: string, deliveryAddress?: WixRestaurantsAddress): Promise<WixFulfillmentTimeSlot[]> {
|
|
191
|
+
return (await getFirstAvailableTimeSlotPerFulfillmentType(operationId, deliveryAddress)).timeslotsPerFulfillmentType ?? [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The pickup address the cart needs before a pickup method can be set: the restaurant's own
|
|
195
|
+
* country (and region when Wix gives one) — Wix requires an address even for pickup. */
|
|
196
|
+
export function pickupCartAddress(method: WixFulfillmentMethod | undefined, fallbackCountry = ""): WixRestaurantsAddress | undefined {
|
|
197
|
+
const address = method?.pickupOptions?.address;
|
|
198
|
+
const country = (address?.country || fallbackCountry).trim();
|
|
199
|
+
if (!country) return undefined;
|
|
200
|
+
return { country, ...(address?.subdivision ? { subdivision: address.subdivision } : {}) };
|
|
201
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { withOnlineWrites } from "../_shared/online-actions.js";
|
|
2
2
|
import { defineService as registerState } from "@himalaya/state";
|
|
3
3
|
import { getStoreProduct } from "../_shared/himi-store.js";
|
|
4
|
+
import { RESTAURANTS_ORDERS_APP_ID } from "../_shared/restaurant-ordering.js";
|
|
4
5
|
import { iapPriceCopy } from "../_shared/iap-worker.js";
|
|
5
6
|
import { withIapPresentation } from "../_shared/iap-presentation.js";
|
|
6
7
|
import { type IapCommerce, classifyCart } from "@himalaya/iap-checkout";
|
|
@@ -523,6 +524,11 @@ const svc = defineService<State, {
|
|
|
523
524
|
const commerce = cartPricing;
|
|
524
525
|
const items = lines.map(line => (line as WixCartLineItemDetail & { catalogReference?: Row["catalogReference"] }).catalogReference);
|
|
525
526
|
if (items.some(item => !item?.catalogItemId || !item.appId)) return derive({ ...state, checkoutHint: iapPriceCopy("cart").purchaseUnavailable });
|
|
527
|
+
// Restaurant dishes share this Wix cart but check out on the food order screen; this checkout
|
|
528
|
+
// takes Stores products only, so say which lines are in the way rather than "unavailable".
|
|
529
|
+
if (items.some(item => item!.appId === RESTAURANTS_ORDERS_APP_ID)) {
|
|
530
|
+
return derive({ ...state, checkoutHint: copyParam("cart.restaurantItemsHint", "This cart also has restaurant dishes. Remove them here to check out your store items.") });
|
|
531
|
+
}
|
|
526
532
|
await commerce.loadPrices(items.map((item, index) => ({ reference: { kind: "storesProduct", id: item!.catalogItemId }, commerceDisplayPrice: lines[index].price?.formattedAmount })));
|
|
527
533
|
const classification = classifyCart(items.map((item, index) => ({ route: commerce.pricing.view({ kind: "storesProduct", id: item!.catalogItemId }).route, quantity: lines[index].quantity ?? 1 })));
|
|
528
534
|
if (classification !== "regular") return derive({ ...state, checkoutHint: classification === "mixedRefused" ? copyParam("cart.iapSingleItemOnly", "Buy digital items individually, with quantity 1 and no options.") : iapPriceCopy("cart").purchaseUnavailable });
|
|
@@ -12,11 +12,15 @@ export interface CollectionSaving {
|
|
|
12
12
|
type State = Record<string, unknown>;
|
|
13
13
|
interface BackendCollection {
|
|
14
14
|
restorePagesOnLoad?: boolean;
|
|
15
|
-
/** Sees the load action's argument (the screen's onAppear params) BEFORE the query runs.
|
|
16
|
-
|
|
15
|
+
/** Sees the load action's argument (the screen's onAppear params) BEFORE the query runs. A returned
|
|
16
|
+
* `{ filter, sort }` resets the criteria for that load (a screen that always opens on one view). */
|
|
17
|
+
onLoad?(arg: unknown): CollectionCriteriaReset | void;
|
|
17
18
|
controller: ReturnType<typeof createCollectionQuery<CollectionRow>>;
|
|
18
19
|
apply(state: State, page: CollectionQueryState<CollectionRow>): State;
|
|
19
20
|
}
|
|
21
|
+
/** A filter and a sort set together (`collectionCriteria`, `onLoad`). Values the collection's contract does
|
|
22
|
+
* not list are ignored. */
|
|
23
|
+
export interface CollectionCriteriaReset { filter?: string; sort?: string; query?: string }
|
|
20
24
|
type Reducer = (s: State, ...args: unknown[]) => State | undefined | Promise<State | undefined>;
|
|
21
25
|
const object = (x: unknown): State => x && typeof x === 'object' && !Array.isArray(x) ? x as State : {};
|
|
22
26
|
const str = (arg: unknown, key: string): string => typeof arg === 'string' ? arg : String(object(arg)[key] ?? '');
|
|
@@ -262,6 +266,16 @@ export function decorateCollection(name: string, def: { state: State; actions?:
|
|
|
262
266
|
if (backend && config.backendQuery && criteriaBefore !== criteriaKey()) return backend.controller.query(criteria()).then(() => undefined);
|
|
263
267
|
return derive(s);
|
|
264
268
|
};
|
|
269
|
+
/** Filter and sort in ONE criteria change, so a tab that sets both runs one query, not two. */
|
|
270
|
+
const setCriteria = (arg: unknown): void => {
|
|
271
|
+
const input = object(arg);
|
|
272
|
+
const sort = String(input.sort ?? '');
|
|
273
|
+
if (sort === 'default' || sort === 'az' || sort === 'za') prefs.sort = sort;
|
|
274
|
+
const filter = String(input.filter ?? '');
|
|
275
|
+
if (filter === 'all' || (filter.startsWith('value:') && (config.filterValues ?? []).includes(filter.slice(6)))) prefs.filter = filter;
|
|
276
|
+
// A screen with no search field clears a query an older screen persisted, or it narrows every view for good.
|
|
277
|
+
if (typeof input.query === 'string') prefs.query = input.query.slice(0, 256);
|
|
278
|
+
};
|
|
265
279
|
const criteria = () => ({ query: prefs.query, sort: prefs.sort, filter: prefs.filter, savedIds: prefs.filter === 'saved' ? prefs.savedIds : [] });
|
|
266
280
|
const criteriaKey = () => JSON.stringify(criteria());
|
|
267
281
|
actions.collectionDismissFilters = change(() => { filtersPresented = false; });
|
|
@@ -273,6 +287,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
|
|
|
273
287
|
actions.collectionSort = change(arg => { const value = str(arg, 'selectedId'); if (value === 'default' || value === 'az' || value === 'za') prefs.sort = value; });
|
|
274
288
|
actions.collectionFilter = change(arg => { const values = object(arg).selectedIds; prefs.filter = Array.isArray(values) && typeof values[0] === 'string' ? values[0] : 'all'; });
|
|
275
289
|
actions.collectionSearch = change(arg => { prefs.query = str(arg, 'value').slice(0, 256); });
|
|
290
|
+
actions.collectionCriteria = change(arg => setCriteria(arg));
|
|
276
291
|
actions.collectionViewName = change(arg => { viewName = str(arg, 'value').slice(0, 40); viewMessage = ''; });
|
|
277
292
|
actions.collectionRememberView = change(() => {
|
|
278
293
|
const name = viewName.trim();
|
|
@@ -305,7 +320,8 @@ export function decorateCollection(name: string, def: { state: State; actions?:
|
|
|
305
320
|
actions.collectionDataChanged = s => derive(backend.apply(s, backend.controller.getState()));
|
|
306
321
|
actions.load = (_s, arg) => {
|
|
307
322
|
stashCopy(arg); syncSaved(); applyNavigationCriteria(arg, _s);
|
|
308
|
-
backend.onLoad?.(arg);
|
|
323
|
+
const reset = backend.onLoad?.(arg);
|
|
324
|
+
if (reset) { setCriteria(reset); persisted.write(prefs); }
|
|
309
325
|
// A load of what the screen already shows is the screen coming back: it revalidates behind its rows.
|
|
310
326
|
return backend.controller.query(criteria(), backend.restorePagesOnLoad, true).then(() => undefined);
|
|
311
327
|
};
|
|
@@ -334,11 +350,13 @@ export function defineBackendCollection<S extends object, Row extends object>(na
|
|
|
334
350
|
register?: typeof register;
|
|
335
351
|
/** Explicit mutation preflight; unrelated collection services keep their current reducers. */
|
|
336
352
|
onlineWrites?: boolean;
|
|
353
|
+
/** Where the offline refusal is written (default `actionMessage`) — the field the screen shows next to the action. */
|
|
354
|
+
onlineWriteMessageFields?: readonly string[];
|
|
337
355
|
/** Revalidate already opened pages on navigation return; explicit refresh resets. */
|
|
338
356
|
restorePagesOnLoad?: boolean;
|
|
339
357
|
/** The load action's argument (onAppear params beyond `copy`), before the query runs — for a
|
|
340
358
|
* domain setting that rides the descriptor, e.g. My Bookings' `weeklyGoal`. */
|
|
341
|
-
onLoad?(arg: unknown): void;
|
|
359
|
+
onLoad?(arg: unknown): CollectionCriteriaReset | void;
|
|
342
360
|
state: S;
|
|
343
361
|
source: CollectionQuerySource<Row>;
|
|
344
362
|
apply(state: S, page: CollectionQueryState<Row>): S;
|
|
@@ -353,7 +371,7 @@ export function defineBackendCollection<S extends object, Row extends object>(na
|
|
|
353
371
|
controller: controller as unknown as BackendCollection['controller'],
|
|
354
372
|
apply: (state, page) => options.apply(state as S, page as unknown as CollectionQueryState<Row>) as State,
|
|
355
373
|
});
|
|
356
|
-
const registerService = options.onlineWrites ? withOnlineWrites(options.register ?? register, ['confirmCancel', 'confirmOperation', 'retryOperation', 'commitWrite'], { messageFields: ['actionMessage'] }) : (options.register ?? register);
|
|
374
|
+
const registerService = options.onlineWrites ? withOnlineWrites(options.register ?? register, ['confirmCancel', 'confirmOperation', 'retryOperation', 'commitWrite'], { messageFields: options.onlineWriteMessageFields ?? ['actionMessage'] }) : (options.register ?? register);
|
|
357
375
|
const handle = registerService(name, definition);
|
|
358
376
|
// Registration restored the state a cold boot saved: the page it showed is this run's, before the first load. Only
|
|
359
377
|
// when the service's own rows came back with it: a bundle that keeps them out of the restore (its `notHydrated`)
|
|
@@ -108,6 +108,24 @@ export function formatMonthDayLong(date: Date, locale = "en-US", timeZone?: stri
|
|
|
108
108
|
return `${MONTHS_LONG[date.getMonth()]} ${date.getDate()}`;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
const WEEKDAYS_LONG = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] as const;
|
|
112
|
+
|
|
113
|
+
/** `{ weekday: "short", month: "short", day: "numeric" }` — "Mon, Jan 5". */
|
|
114
|
+
export function formatWeekdayMonthDay(date: Date, locale = "en-US", timeZone?: string): string {
|
|
115
|
+
if (hasIntl()) {
|
|
116
|
+
return dateFormat(locale, { weekday: "short", month: "short", day: "numeric" }, timeZone).format(date);
|
|
117
|
+
}
|
|
118
|
+
return `${WEEKDAYS_SHORT[date.getDay()]}, ${MONTHS_SHORT[date.getMonth()]} ${date.getDate()}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** `{ weekday: "long", month: "long", day: "numeric" }` — "Monday, January 5". */
|
|
122
|
+
export function formatWeekdayMonthDayLong(date: Date, locale = "en-US", timeZone?: string): string {
|
|
123
|
+
if (hasIntl()) {
|
|
124
|
+
return dateFormat(locale, { weekday: "long", month: "long", day: "numeric" }, timeZone).format(date);
|
|
125
|
+
}
|
|
126
|
+
return `${WEEKDAYS_LONG[date.getDay()]}, ${MONTHS_LONG[date.getMonth()]} ${date.getDate()}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
111
129
|
/** `{ weekday: "short", month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }`
|
|
112
130
|
* — "Mon, Jan 5, 3:07 PM". */
|
|
113
131
|
export function formatWeekdayDateTime(date: Date, locale = "en-US", timeZone?: string): string {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# __HIMI_APP_NAME__ — from the `restaurant` template
|
|
2
2
|
|
|
3
|
-
Restaurant / café / bar app over a real Wix site, with a menu-first home: section-grouped menu browsing (Wix Restaurants Menus) with dish detail screens, table-reservation requests (hold → reserve on the visitor rung), popular dishes, location and opening hours, plus wine-night events and a kitchen blog. Boots as a visitor (the public menu paints with no login); the reservation form submits anonymously and degrades in-content when online booking isn't set up.
|
|
3
|
+
Restaurant / café / bar app over a real Wix site, with a menu-first home: section-grouped menu browsing (Wix Restaurants Menus) with dish detail screens, online ordering into the site's Wix cart (pickup or delivery, the site's own hosted checkout, and a signed-in member's past restaurant orders), table-reservation requests (hold → reserve on the visitor rung), popular dishes, location and opening hours, plus wine-night events and a kitchen blog. Boots as a visitor (the public menu paints with no login); the reservation form submits anonymously and degrades in-content when online booking isn't set up.
|
|
4
4
|
|
|
5
5
|
Built on the same screens, workers and sign-in model as the `branded-lite` template, set up for restaurants + events + blog with the `menuFirst` home layout. See the pattern index below for where each recipe lives in this template's screens.
|
|
6
6
|
|
|
@@ -356,7 +356,7 @@ export default defineApp({
|
|
|
356
356
|
// Your on-device database. It must declare every entity a worker reads or writes,
|
|
357
357
|
// or those calls fail closed — keep it in step with `local_database` above.
|
|
358
358
|
dataSchema: {
|
|
359
|
-
"version":
|
|
359
|
+
"version": 2,
|
|
360
360
|
"entities": [
|
|
361
361
|
{
|
|
362
362
|
"name": "products",
|
|
@@ -481,6 +481,17 @@ export default defineApp({
|
|
|
481
481
|
}
|
|
482
482
|
]
|
|
483
483
|
},
|
|
484
|
+
{
|
|
485
|
+
"name": "food_orders",
|
|
486
|
+
"encrypted": true,
|
|
487
|
+
"fields": [
|
|
488
|
+
{
|
|
489
|
+
"name": "id",
|
|
490
|
+
"type": "string",
|
|
491
|
+
"required": true
|
|
492
|
+
}
|
|
493
|
+
]
|
|
494
|
+
},
|
|
484
495
|
{
|
|
485
496
|
"name": "member_bookings",
|
|
486
497
|
"encrypted": true,
|