@wix/himalaya-cli 0.1488.0 → 0.1490.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/core/dev-server/src/builder.d.ts +12 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +13 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/bookings-v2/src/extensions.d.ts +48 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-bookings.mjs +22 -0
- package/dist/cli.mjs +157 -48
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/widgets.md +20 -7
- package/package.json +2 -2
- package/templates/beauty-salon/config.ts +13 -0
- package/templates/beauty-salon/dev/index.ts +784 -0
- package/templates/beauty-salon/himalaya.content.json +3 -0
- package/templates/beauty-salon/tier5-src/_shared/branded-lite-hosted.ts +2 -1
- package/templates/beauty-salon/tier5-src/_shared/branded-load-state.ts +106 -0
- package/templates/beauty-salon/tier5-src/_shared/himi-bookings.ts +14 -0
- package/templates/beauty-salon/tier5-src/_shared/member-account-routes.ts +2 -1
- package/templates/beauty-salon/tier5-src/himi-booking-time/index.ts +25 -11
- package/templates/beauty-salon/tier5-src/himi-branded-lite-agenda/index.ts +769 -0
- package/templates/branded-lite/config.ts +13 -0
- package/templates/branded-lite/dev/index.ts +784 -0
- package/templates/branded-lite/himalaya.content.json +3 -0
- package/templates/branded-lite/tier5-src/_shared/branded-lite-hosted.ts +2 -1
- package/templates/branded-lite/tier5-src/_shared/himi-bookings.ts +14 -0
- package/templates/branded-lite/tier5-src/_shared/member-account-routes.ts +2 -1
- package/templates/branded-lite/tier5-src/himi-booking-time/index.ts +25 -11
- package/templates/branded-lite/tier5-src/himi-branded-lite-agenda/index.ts +769 -0
- package/templates/community-club/tier5-src/_shared/branded-lite-hosted.ts +2 -1
- package/templates/community-club/tier5-src/_shared/member-account-routes.ts +2 -1
- package/templates/events-meetups/tier5-src/_shared/branded-lite-hosted.ts +2 -1
- package/templates/events-meetups/tier5-src/_shared/member-account-routes.ts +2 -1
- package/templates/gym-fitness/config.ts +13 -0
- package/templates/gym-fitness/dev/index.ts +784 -0
- package/templates/gym-fitness/himalaya.content.json +3 -0
- package/templates/gym-fitness/tier5-src/_shared/branded-lite-hosted.ts +2 -1
- package/templates/gym-fitness/tier5-src/_shared/branded-load-state.ts +106 -0
- package/templates/gym-fitness/tier5-src/_shared/himi-bookings.ts +14 -0
- package/templates/gym-fitness/tier5-src/_shared/member-account-routes.ts +2 -1
- package/templates/gym-fitness/tier5-src/himi-booking-time/index.ts +25 -11
- package/templates/gym-fitness/tier5-src/himi-branded-lite-agenda/index.ts +769 -0
- package/templates/nonprofit-cause/tier5-src/_shared/branded-lite-hosted.ts +2 -1
- package/templates/nonprofit-cause/tier5-src/_shared/member-account-routes.ts +2 -1
- package/templates/podcast-creator/tier5-src/_shared/branded-lite-hosted.ts +2 -1
- package/templates/podcast-creator/tier5-src/_shared/member-account-routes.ts +2 -1
- package/templates/portfolio-creative/tier5-src/_shared/branded-lite-hosted.ts +2 -1
- package/templates/portfolio-creative/tier5-src/_shared/member-account-routes.ts +2 -1
- package/templates/restaurant/tier5-src/_shared/branded-lite-hosted.ts +2 -1
- package/templates/restaurant/tier5-src/_shared/member-account-routes.ts +2 -1
- package/templates/retail-store/tier5-src/_shared/branded-lite-hosted.ts +2 -1
- package/templates/retail-store/tier5-src/_shared/member-account-routes.ts +2 -1
- package/templates/yoga-studio/config.ts +13 -0
- package/templates/yoga-studio/dev/index.ts +784 -0
- package/templates/yoga-studio/himalaya.content.json +3 -0
- package/templates/yoga-studio/tier5-src/_shared/branded-lite-hosted.ts +2 -1
- package/templates/yoga-studio/tier5-src/_shared/branded-load-state.ts +106 -0
- package/templates/yoga-studio/tier5-src/_shared/himi-bookings.ts +14 -0
- package/templates/yoga-studio/tier5-src/_shared/member-account-routes.ts +2 -1
- package/templates/yoga-studio/tier5-src/himi-booking-time/index.ts +25 -11
- package/templates/yoga-studio/tier5-src/himi-branded-lite-agenda/index.ts +769 -0
|
@@ -48,7 +48,7 @@ export const HOSTED_ALWAYS: readonly BrandedLiteVertical[] = ["members"];
|
|
|
48
48
|
|
|
49
49
|
/** The screen each vertical's nav rows open, so a route to an uninstalled vertical can be answered. */
|
|
50
50
|
export const VERTICAL_ROOT_SCREENS: Readonly<Record<string, readonly string[]>> = {
|
|
51
|
-
store: ["himi_shop"], bookings: ["himi_services"], restaurants: ["himi_menu", "himi_reserve"],
|
|
51
|
+
store: ["himi_shop"], bookings: ["himi_services", "himi_agenda"], restaurants: ["himi_menu", "himi_reserve"],
|
|
52
52
|
programs: ["himi_programs"], plans: ["himi_plans"], feed: ["himi_feed"], members: ["himi_members"],
|
|
53
53
|
forum: ["himi_forum"], coupons: ["himi_coupons"], notifications: ["himi_notifications"],
|
|
54
54
|
loyalty: ["himi_loyalty"], reviews: ["himi_reviews"], portfolio: ["himi_portfolio"],
|
|
@@ -67,6 +67,7 @@ export const VERTICAL_ROOT_SCREENS: Readonly<Record<string, readonly string[]>>
|
|
|
67
67
|
export const HOSTED_SCREEN_VERTICALS: Readonly<Record<string, readonly BrandedLiteVertical[]>> = {
|
|
68
68
|
himi_address: ["restaurants", "store"],
|
|
69
69
|
himi_addresses: ["restaurants", "store"],
|
|
70
|
+
himi_agenda: ["bookings"],
|
|
70
71
|
himi_badge: ["badges"],
|
|
71
72
|
himi_badges: ["badges"],
|
|
72
73
|
himi_blog: ["blog"],
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Load-state honesty for the branded-lite vertical workers.
|
|
2
|
+
//
|
|
3
|
+
// These workers used to answer every failed or empty query with representative SAMPLE
|
|
4
|
+
// rows — discount codes, gift-card balances, invoices, reservations, inbox messages —
|
|
5
|
+
// under a "Sample content" banner. On a customer build that is invented data presented
|
|
6
|
+
// as the customer's own: a code that does not exist, a balance in the wrong currency, an
|
|
7
|
+
// "Overdue" invoice, a booking labelled "tap to cancel". The banner did not make it
|
|
8
|
+
// honest; the rows were still the first thing a visitor read.
|
|
9
|
+
//
|
|
10
|
+
// The rule now:
|
|
11
|
+
// • query FAILED → status "error" with a customer sentence, a Retry, and — when
|
|
12
|
+
// Wix refused an anonymous visitor (401) — a Sign in path instead;
|
|
13
|
+
// • query was EMPTY → status "ready", no rows, the vertical's own empty sentence;
|
|
14
|
+
// • sample rows → ONLY when the host explicitly asked for sample data (the web
|
|
15
|
+
// preview's sample data mode, `__himiOwnerPreview: "sample"`).
|
|
16
|
+
// Never on a device, never as a fallback.
|
|
17
|
+
//
|
|
18
|
+
// Every field here is flat and boolean-gated because descriptors can only
|
|
19
|
+
// `hiddenWhen(<path>)` on a TRUE value: `notErrored` hides the error card, `noSignIn`
|
|
20
|
+
// hides the sign-in CTA, `signInRequired` hides Retry when retrying cannot help.
|
|
21
|
+
import { copyParam } from "./himi-copy.js";
|
|
22
|
+
|
|
23
|
+
export interface LoadFlags {
|
|
24
|
+
status: "loading" | "ready" | "error";
|
|
25
|
+
/** The customer-facing failure sentence; "" unless status is "error". */
|
|
26
|
+
error: string;
|
|
27
|
+
signInRequired: boolean;
|
|
28
|
+
notErrored: boolean;
|
|
29
|
+
/** hiddenWhen gate for a loading skeleton. */
|
|
30
|
+
notLoading: boolean;
|
|
31
|
+
/** Loading or failed — gates content that has nothing to say in either. */
|
|
32
|
+
busy: boolean;
|
|
33
|
+
noSignIn: boolean;
|
|
34
|
+
isMock: boolean;
|
|
35
|
+
notMock: boolean;
|
|
36
|
+
notMockEmpty: boolean;
|
|
37
|
+
notMockUnavailable: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const LOADING: LoadFlags = {
|
|
41
|
+
status: "loading", error: "", signInRequired: false, notErrored: true, notLoading: false, busy: true, noSignIn: true,
|
|
42
|
+
isMock: false, notMock: true, notMockEmpty: true, notMockUnavailable: true,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const READY: LoadFlags = { ...LOADING, status: "ready", notLoading: true, busy: false };
|
|
46
|
+
|
|
47
|
+
/** Did the HOST ask for sample data? Only the web preview's sample mode does; a native
|
|
48
|
+
* build, a visitor preview and a live owner preview never carry it. */
|
|
49
|
+
export function samplesRequested(): boolean {
|
|
50
|
+
const input = (globalThis as { himalaya?: { input?: Record<string, unknown> } }).himalaya?.input;
|
|
51
|
+
return input?.__himiOwnerPreview === "sample";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Wix refused an ANONYMOUS visitor: signing in is the fix, not retrying. A 403 is not
|
|
55
|
+
* this — it also answers "feature not installed" and a signed-in member without the
|
|
56
|
+
* permission, where a Sign in button leads nowhere — so it stays a Retry-able failure. */
|
|
57
|
+
export function needsSignIn(err: unknown): boolean {
|
|
58
|
+
const msg = String((err as { message?: unknown })?.message ?? err ?? "");
|
|
59
|
+
return /\b401\b|unauthori[sz]ed|unauthenticated/i.test(msg);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface LoadCopy<T> {
|
|
63
|
+
/** The copy namespace (`giftCards`, `coupons`, …) — keys resolve through copyParam. */
|
|
64
|
+
prefix: string;
|
|
65
|
+
/** Shown when the site genuinely has none. */
|
|
66
|
+
empty: string;
|
|
67
|
+
/** Shown when the query failed. */
|
|
68
|
+
error: string;
|
|
69
|
+
/** Shown when Wix refused the visitor (member-only data). */
|
|
70
|
+
signIn: string;
|
|
71
|
+
/** Sample rows for the explicit sample preview ONLY. */
|
|
72
|
+
sample: () => T[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type Rows<K extends string, T> = { [P in K]: T[] } & { emptyText: string } & LoadFlags;
|
|
76
|
+
|
|
77
|
+
/** A query answered. Rows win; an empty answer is a real empty state. */
|
|
78
|
+
export function loaded<K extends string, T>(key: K, rows: T[], copy: LoadCopy<T>): Rows<K, T> {
|
|
79
|
+
if (rows.length) return { ...READY, [key]: rows, emptyText: "" } as Rows<K, T>;
|
|
80
|
+
if (samplesRequested()) return sampleRows(key, copy, "empty");
|
|
81
|
+
return { ...READY, [key]: [], emptyText: copyParam(`${copy.prefix}.emptyText`, copy.empty) } as Rows<K, T>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A query threw. Never rows the customer did not create. */
|
|
85
|
+
export function loadFailed<K extends string, T>(key: K, err: unknown, copy: LoadCopy<T>): Rows<K, T> {
|
|
86
|
+
if (samplesRequested()) return sampleRows(key, copy, "unavailable");
|
|
87
|
+
const signIn = needsSignIn(err);
|
|
88
|
+
return {
|
|
89
|
+
...READY,
|
|
90
|
+
status: "error",
|
|
91
|
+
[key]: [],
|
|
92
|
+
emptyText: "",
|
|
93
|
+
error: signIn ? copyParam(`${copy.prefix}.signInText`, copy.signIn) : copyParam(`${copy.prefix}.errorText`, copy.error),
|
|
94
|
+
signInRequired: signIn,
|
|
95
|
+
notErrored: false,
|
|
96
|
+
busy: true,
|
|
97
|
+
noSignIn: !signIn,
|
|
98
|
+
} as Rows<K, T>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sampleRows<K extends string, T>(key: K, copy: LoadCopy<T>, reason: "empty" | "unavailable"): Rows<K, T> {
|
|
102
|
+
return {
|
|
103
|
+
...READY, [key]: copy.sample(), emptyText: "",
|
|
104
|
+
isMock: true, notMock: false, notMockEmpty: reason !== "empty", notMockUnavailable: reason !== "unavailable",
|
|
105
|
+
} as Rows<K, T>;
|
|
106
|
+
}
|
|
@@ -39,6 +39,20 @@ export function slotStartInstant(slot: { startDate?: string; timezone?: unknown
|
|
|
39
39
|
return wall - (Number.isFinite(again) ? again : offset);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** `himi_time` reads the next two weeks. A host's `start` past them (an Agenda session weeks
|
|
43
|
+
* ahead) is read on its own — that session's local day only, never everything up to it — so it
|
|
44
|
+
* can be preselected while each read stays bounded. `undefined` when the start is inside the two
|
|
45
|
+
* weeks. */
|
|
46
|
+
export function availabilityBeyond(now: Date, start = ""): { from: Date; until: Date } | undefined {
|
|
47
|
+
const end = now.getTime() + 14 * 86_400_000;
|
|
48
|
+
const at = start ? Date.parse(start) : NaN;
|
|
49
|
+
if (!Number.isFinite(at) || at < end) return undefined;
|
|
50
|
+
const d = new Date(at);
|
|
51
|
+
const dayStart = new Date(d.getFullYear(), d.getMonth(), d.getDate());
|
|
52
|
+
// Never back into the two weeks already read: a slot in both would be listed twice.
|
|
53
|
+
return { from: new Date(Math.max(dayStart.getTime(), end)), until: new Date(d.getFullYear(), d.getMonth(), d.getDate() + 1) };
|
|
54
|
+
}
|
|
55
|
+
|
|
42
56
|
/** The slot a host asked to open at (`himi_time` input `start`, an ISO 8601 instant): the id of the
|
|
43
57
|
* slot that starts at that same instant, or "" when none does (taken since, or out of the window).
|
|
44
58
|
* Compared as instants, so `…T16:30:00Z` finds a slot listed as `…T18:30:00+02:00`, or as a local
|
|
@@ -18,7 +18,8 @@ export const MEMBER_ACCOUNT_ROUTES_MIN_BINARY = "1.4.0";
|
|
|
18
18
|
export const MEMBER_MEDIA_FIELD_MIN_BINARY = "1.5.0";
|
|
19
19
|
export const MEMBER_PASSWORD_ACTIONS_MIN_BINARY = "1.5.0";
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
/** `version` (a HimalayaCore binary version, pre-release suffix ignored) is `floor` or later. */
|
|
22
|
+
export function atLeast(version: string, floor: string): boolean {
|
|
22
23
|
const parts = (v: string) => v.split("-")[0].split(".").map((p) => Number.parseInt(p, 10) || 0);
|
|
23
24
|
const a = parts(version), b = parts(floor);
|
|
24
25
|
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
@@ -17,7 +17,7 @@ import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
|
17
17
|
import { upsert as dbUpsert, remove as dbRemove, enqueue as dbEnqueue, type DbEntity } from "@himalaya/local-db";
|
|
18
18
|
import { isBridgeUnavailable } from "../_shared/owner-load-error.js";
|
|
19
19
|
import { emitHostEvent, hostEventInstant } from "../_shared/host-events.js";
|
|
20
|
-
import { slotIdAt } from "../_shared/himi-bookings.js";
|
|
20
|
+
import { availabilityBeyond, slotIdAt } from "../_shared/himi-bookings.js";
|
|
21
21
|
import { wixMediaUrl, WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
|
|
22
22
|
|
|
23
23
|
// Offline-first (data-offline): booking a slot is WRITE-BEHIND against the
|
|
@@ -167,6 +167,15 @@ let pendingStart = (() => {
|
|
|
167
167
|
const start = (globalThis as { himalaya?: { input?: { start?: unknown } } }).himalaya?.input?.start;
|
|
168
168
|
return typeof start === "string" && Number.isFinite(Date.parse(start)) ? start : "";
|
|
169
169
|
})();
|
|
170
|
+
/** The exact session a host opened this screen at (`himalaya.input.sessionId`), when it names one:
|
|
171
|
+
* two sessions of a class can start at the same instant, and `start` alone picks the first. */
|
|
172
|
+
/** A host's `start` past the two-week window, kept for the life of this worker: a pick on that day
|
|
173
|
+
* clears `pendingStart`, yet the reload after the guest sign-in step must still read it. */
|
|
174
|
+
const farStart = pendingStart;
|
|
175
|
+
let pendingSession = (() => {
|
|
176
|
+
const id = (globalThis as { himalaya?: { input?: { sessionId?: unknown } } }).himalaya?.input?.sessionId;
|
|
177
|
+
return typeof id === "string" ? id.trim() : "";
|
|
178
|
+
})();
|
|
170
179
|
|
|
171
180
|
function strOf(arg: unknown): string {
|
|
172
181
|
if (typeof arg === "string") return arg;
|
|
@@ -197,12 +206,6 @@ function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
|
197
206
|
});
|
|
198
207
|
}
|
|
199
208
|
|
|
200
|
-
function addDays(date: Date, days: number): Date {
|
|
201
|
-
const next = new Date(date);
|
|
202
|
-
next.setUTCDate(next.getUTCDate() + days);
|
|
203
|
-
return next;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
209
|
function isoDate(date: Date): string {
|
|
207
210
|
return date.toISOString();
|
|
208
211
|
}
|
|
@@ -559,16 +562,21 @@ const svc = defineService<S, {
|
|
|
559
562
|
let next = derive({ ...s, status: "loading", errored: false, message: "", successMessage: "" });
|
|
560
563
|
next = await probeMember(next);
|
|
561
564
|
try {
|
|
562
|
-
const
|
|
565
|
+
const now = new Date();
|
|
566
|
+
const beyond = availabilityBeyond(now, farStart);
|
|
567
|
+
const [serviceRes, availability, target] = await Promise.all([
|
|
563
568
|
getService(s.id).catch(() => ({ service: undefined })),
|
|
564
|
-
queryServiceAvailability(s.id, isoDate(
|
|
569
|
+
queryServiceAvailability(s.id, isoDate(now), isoDate(new Date(now.getTime() + WINDOW_DAYS * 86_400_000))),
|
|
570
|
+
// A host's start past the window (an Agenda session weeks out): its day alone, so it can
|
|
571
|
+
// be preselected. Its failure costs only that preselection.
|
|
572
|
+
beyond ? queryServiceAvailability(s.id, isoDate(beyond.from), isoDate(beyond.until)).catch(() => ({ availabilityEntries: [] })) : undefined,
|
|
565
573
|
]);
|
|
566
574
|
slotsById = new Map();
|
|
567
575
|
const rows: SlotRow[] = [];
|
|
568
576
|
// Group by LOCAL calendar day, not by the formatted heading: two
|
|
569
577
|
// distinct days can render the same text, and a UTC-derived key can
|
|
570
578
|
// name a different day than the heading beside it.
|
|
571
|
-
for (const [index, entry] of (availability.availabilityEntries ?? [])
|
|
579
|
+
for (const [index, entry] of [...(availability.availabilityEntries ?? []), ...(target?.availabilityEntries ?? [])]
|
|
572
580
|
.filter((candidate) => candidate.bookable !== false && candidate.slot?.startDate)
|
|
573
581
|
.entries()) {
|
|
574
582
|
const slot = entry.slot as WixBookingSlotFull;
|
|
@@ -595,7 +603,11 @@ const svc = defineService<S, {
|
|
|
595
603
|
const chosen: S = selectionRevision !== revisionAtStart ? (svc.getState?.() ?? next) : next;
|
|
596
604
|
// A host's `start` (deep entry): that slot preselected, or — taken since — its day.
|
|
597
605
|
const wantedStart = pendingStart;
|
|
598
|
-
|
|
606
|
+
// The named session (a slot row's id IS its session id), else the slot at that instant. A
|
|
607
|
+
// named session that is gone is "taken since" — never another session at the same time.
|
|
608
|
+
const startRow = !wantedStart ? undefined
|
|
609
|
+
: pendingSession ? rows.find((r) => r.id === pendingSession)
|
|
610
|
+
: rows.find((r) => r.id === slotIdAt(slotsById, wantedStart));
|
|
599
611
|
// The requested day even with no time left on it: "taken since" opens there, nothing selected.
|
|
600
612
|
const startDay = wantedStart ? days.find((d) => d.id === dayKey(wantedStart)) : undefined;
|
|
601
613
|
const selectionAlive = !!startRow || slotsById.has(chosen.selectedSlotId);
|
|
@@ -638,6 +650,7 @@ const svc = defineService<S, {
|
|
|
638
650
|
const day = chosenDay(arg);
|
|
639
651
|
if (!day || !s.days.some((d) => d.id === day)) return derive(s);
|
|
640
652
|
pendingStart = "";
|
|
653
|
+
pendingSession = "";
|
|
641
654
|
return derive(withDay(s, day));
|
|
642
655
|
},
|
|
643
656
|
refreshMember: (s, arg) => { stashCopy(arg); return probeMember(s); },
|
|
@@ -649,6 +662,7 @@ const svc = defineService<S, {
|
|
|
649
662
|
if (selected) {
|
|
650
663
|
selectionRevision++;
|
|
651
664
|
pendingStart = "";
|
|
665
|
+
pendingSession = "";
|
|
652
666
|
if (s.isMember) clearPick();
|
|
653
667
|
else rememberPick(s.id, slotId);
|
|
654
668
|
}
|