@wix/himalaya-cli 0.1492.0 → 0.1494.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 +2 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/bookings-v2/src/extensions.d.ts +52 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/bookings-v2/src/index.d.ts +180 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-bookings.mjs +4 -2
- package/dist/cli.mjs +30 -29
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/qa-builds.md +14 -2
- package/guides/sdk-reference.json +256 -11
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +2273 -520
- package/templates/beauty-salon/himalaya.content.json +3 -0
- package/templates/beauty-salon/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/beauty-salon/tier5-src/_shared/wix-service-price.ts +18 -0
- package/templates/beauty-salon/tier5-src/himi-booking-service/index.ts +514 -117
- package/templates/beauty-salon/tier5-src/himi-branded-lite-place-summary/index.ts +208 -0
- package/templates/beauty-salon/tokens.json +6 -0
- package/templates/branded-lite/dev/index.ts +2037 -277
- package/templates/branded-lite/himalaya.content.json +3 -0
- package/templates/branded-lite/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/branded-lite/tier5-src/_shared/wix-service-price.ts +18 -0
- package/templates/branded-lite/tier5-src/himi-booking-service/index.ts +514 -117
- package/templates/branded-lite/tier5-src/himi-branded-lite-place-summary/index.ts +208 -0
- package/templates/branded-lite/tokens.json +1 -0
- package/templates/community-club/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/community-club/tier5-src/_shared/wix-service-price.ts +18 -0
- package/templates/community-club/tokens.json +6 -0
- package/templates/events-meetups/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/events-meetups/tier5-src/_shared/wix-service-price.ts +18 -0
- package/templates/events-meetups/tokens.json +6 -0
- package/templates/gym-fitness/dev/index.ts +2273 -520
- package/templates/gym-fitness/himalaya.content.json +3 -0
- package/templates/gym-fitness/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/gym-fitness/tier5-src/_shared/wix-service-price.ts +18 -0
- package/templates/gym-fitness/tier5-src/himi-booking-service/index.ts +514 -117
- package/templates/gym-fitness/tier5-src/himi-branded-lite-place-summary/index.ts +208 -0
- package/templates/gym-fitness/tokens.json +6 -0
- package/templates/nonprofit-cause/dev/index.ts +399 -0
- package/templates/nonprofit-cause/himalaya.content.json +3 -0
- package/templates/nonprofit-cause/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/nonprofit-cause/tier5-src/_shared/wix-service-price.ts +18 -0
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-place-summary/index.ts +208 -0
- package/templates/nonprofit-cause/tokens.json +6 -0
- package/templates/podcast-creator/dev/index.ts +399 -0
- package/templates/podcast-creator/himalaya.content.json +3 -0
- package/templates/podcast-creator/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/podcast-creator/tier5-src/_shared/wix-service-price.ts +18 -0
- package/templates/podcast-creator/tier5-src/himi-branded-lite-place-summary/index.ts +208 -0
- package/templates/podcast-creator/tokens.json +6 -0
- package/templates/portfolio-creative/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/portfolio-creative/tier5-src/_shared/wix-service-price.ts +18 -0
- package/templates/portfolio-creative/tokens.json +6 -0
- package/templates/restaurant/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/restaurant/tier5-src/_shared/wix-service-price.ts +18 -0
- package/templates/restaurant/tokens.json +6 -0
- package/templates/retail-store/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/retail-store/tier5-src/_shared/wix-service-price.ts +18 -0
- package/templates/retail-store/tokens.json +6 -0
- package/templates/yoga-studio/dev/index.ts +2868 -1115
- package/templates/yoga-studio/himalaya.content.json +3 -0
- package/templates/yoga-studio/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/yoga-studio/tier5-src/_shared/wix-service-price.ts +18 -0
- package/templates/yoga-studio/tier5-src/himi-booking-service/index.ts +514 -117
- package/templates/yoga-studio/tier5-src/himi-branded-lite-place-summary/index.ts +208 -0
- package/templates/yoga-studio/tokens.json +6 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// Tier-5 bundle: himi-branded-lite-place-summary — the member's place at a glance.
|
|
2
|
+
//
|
|
3
|
+
// A compact screen a host embeds at a tile height (a Home tile, a collapsed bar above a chat):
|
|
4
|
+
// the member's next booking, their active plan and ONE primary action. Stable host entry point
|
|
5
|
+
// `himi_place_summary` (README → Embedded in a host app). It reuses the reads My bookings
|
|
6
|
+
// (`_shared/member-bookings.ts`) and the plan entitlement (`listMyPlanOrders`) already make, and
|
|
7
|
+
// adds none of its own.
|
|
8
|
+
//
|
|
9
|
+
// `load` is both the onAppear action and what an embed controller's `refresh()` runs (arg-less:
|
|
10
|
+
// the stashed appear params keep the verticals and the copy pack), so a host that hears
|
|
11
|
+
// `booking.created` / `booking.cancelled` re-reads the summary without a remount.
|
|
12
|
+
import { defineService } from "@himalaya/state";
|
|
13
|
+
import { listMyPlanOrders, type WixPlanOrder } from "@himalaya/wix-pricing-plans";
|
|
14
|
+
import { fetchMemberBookings, type MemberBookingRow } from "../_shared/member-bookings.js";
|
|
15
|
+
import { memberAccess, assertMemberAccess, memberAccessDenied, type MemberAccess } from "../_shared/member-access.js";
|
|
16
|
+
import { hostSiteFromInput } from "../_shared/branded-lite-hosted.js";
|
|
17
|
+
import { formatDateMedium } from "../_shared/engine-intl.js";
|
|
18
|
+
import { copyParam, copyTemplate, stashCopy } from "../_shared/himi-copy.js";
|
|
19
|
+
|
|
20
|
+
export interface PlaceSummaryState {
|
|
21
|
+
status: "loading" | "ready" | "error";
|
|
22
|
+
isLoading: boolean;
|
|
23
|
+
notLoading: boolean;
|
|
24
|
+
hasBooking: boolean;
|
|
25
|
+
noBooking: boolean;
|
|
26
|
+
bookingId: string;
|
|
27
|
+
bookingDay: string;
|
|
28
|
+
bookingTitle: string;
|
|
29
|
+
bookingWhen: string;
|
|
30
|
+
/** The booking row's whole spoken label: the row's label replaces its children. */
|
|
31
|
+
bookingA11y: string;
|
|
32
|
+
hasPlan: boolean;
|
|
33
|
+
noPlan: boolean;
|
|
34
|
+
planName: string;
|
|
35
|
+
planDetail: string;
|
|
36
|
+
/** The one-line state shown instead of a booking and a plan: empty, guest or load failure. */
|
|
37
|
+
hasMessage: boolean;
|
|
38
|
+
noMessage: boolean;
|
|
39
|
+
messageTitle: string;
|
|
40
|
+
messageBody: string;
|
|
41
|
+
errored: boolean;
|
|
42
|
+
notErrored: boolean;
|
|
43
|
+
ctaLabel: string;
|
|
44
|
+
ctaScreenId: string;
|
|
45
|
+
/** `himi_sign_in`'s mode for a guest's Sign in; "" for every other action. */
|
|
46
|
+
ctaMode: string;
|
|
47
|
+
hideCta: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const BOOKINGS_SCREEN = "himi_services";
|
|
51
|
+
const PLANS_SCREEN = "himi_plans";
|
|
52
|
+
const SIGN_IN_SCREEN = "himi_sign_in";
|
|
53
|
+
|
|
54
|
+
let stashedVerticals: string[] | null = null;
|
|
55
|
+
let generation = 0;
|
|
56
|
+
|
|
57
|
+
function appearVerticals(arg: unknown): string[] {
|
|
58
|
+
const raw = arg && typeof arg === "object" ? (arg as Record<string, unknown>).verticals : undefined;
|
|
59
|
+
if (typeof raw === "string") {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
62
|
+
if (Array.isArray(parsed)) stashedVerticals = parsed.filter((v): v is string => typeof v === "string");
|
|
63
|
+
} catch { /* keep the last good list */ }
|
|
64
|
+
}
|
|
65
|
+
return stashedVerticals ?? ["bookings", "plans"];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The verticals this summary reads: the build's, narrowed to the host's site when a host
|
|
69
|
+
* opened a hosted build for one. Native core always writes `__himiHostSite` — `""` when not
|
|
70
|
+
* hosted — so absent or `""` is a deep entry (the build's verticals); anything else is a host's
|
|
71
|
+
* site, and one that does not parse installs nothing (fail closed, `readHostedSite`). */
|
|
72
|
+
export function summaryVerticals(buildVerticals: string[], input: unknown): { bookings: boolean; plans: boolean } {
|
|
73
|
+
const build = new Set(buildVerticals);
|
|
74
|
+
const raw = input && typeof input === "object" ? (input as Record<string, unknown>).__himiHostSite : undefined;
|
|
75
|
+
const hosted = raw !== undefined && raw !== null && raw !== "";
|
|
76
|
+
const installed = hosted ? new Set(hostSiteFromInput(input)?.installedVerticals ?? []) : build;
|
|
77
|
+
return { bookings: build.has("bookings") && installed.has("bookings"), plans: build.has("plans") && installed.has("plans") };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The member's next upcoming booking, earliest first, or null. */
|
|
81
|
+
export function nextBooking(rows: MemberBookingRow[], now: number): MemberBookingRow | null {
|
|
82
|
+
return rows
|
|
83
|
+
.filter((row) => Date.parse(row.start) > now && !["CANCELED", "CANCELLED", "DECLINED"].includes(row.status))
|
|
84
|
+
.sort((a, b) => Date.parse(a.start) - Date.parse(b.start))[0] ?? null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The plan line: the active plan's name and when its current term ends. A member cannot read
|
|
88
|
+
* their remaining sessions or credits (Benefit Programs balances are APP-identity only), so the
|
|
89
|
+
* line names the plan and its term rather than inventing a count. */
|
|
90
|
+
export function planLine(orders: WixPlanOrder[]): { name: string; detail: string } | null {
|
|
91
|
+
const active = orders.filter((o) => (o.status ?? "").toUpperCase() === "ACTIVE" && o.planName);
|
|
92
|
+
const first = active[0];
|
|
93
|
+
if (!first) return null;
|
|
94
|
+
// An order that will end (a fixed term, a cancel at period end, the last cycle) is valid
|
|
95
|
+
// until its end; only an open-ended recurring order renews at its cycle's end.
|
|
96
|
+
const ends = [first.earliestEndDate, first.endDate].filter((d): d is string => !!d && Number.isFinite(Date.parse(d)));
|
|
97
|
+
const end = ends.sort((a, b) => Date.parse(a) - Date.parse(b))[0] ?? "";
|
|
98
|
+
const recurring = !end && !!first.currentCycle?.endedDate;
|
|
99
|
+
const until = end || first.currentCycle?.endedDate || "";
|
|
100
|
+
const when = until && Number.isFinite(Date.parse(until)) ? formatDateMedium(new Date(until)) : "";
|
|
101
|
+
let detail = when
|
|
102
|
+
? recurring
|
|
103
|
+
? copyTemplate("placeSummary.planRenews", "Renews {date}", { date: when })
|
|
104
|
+
: copyTemplate("placeSummary.planUntil", "Valid until {date}", { date: when })
|
|
105
|
+
: copyParam("placeSummary.planActive", "Active");
|
|
106
|
+
if (active.length > 1) detail = `${detail} · ${copyTemplate("placeSummary.planMore", "+{count} more", { count: active.length - 1 })}`;
|
|
107
|
+
return { name: first.planName ?? "", detail };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function derive(s: PlaceSummaryState): PlaceSummaryState {
|
|
111
|
+
return {
|
|
112
|
+
...s,
|
|
113
|
+
isLoading: s.status === "loading",
|
|
114
|
+
notLoading: s.status !== "loading",
|
|
115
|
+
noBooking: !s.hasBooking,
|
|
116
|
+
noPlan: !s.hasPlan,
|
|
117
|
+
noMessage: !s.hasMessage,
|
|
118
|
+
notErrored: !s.errored,
|
|
119
|
+
hideCta: s.status === "loading" || s.errored || !s.ctaScreenId,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const BLANK: PlaceSummaryState = derive({
|
|
124
|
+
status: "loading", isLoading: true, notLoading: false,
|
|
125
|
+
hasBooking: false, noBooking: true, bookingId: "", bookingDay: "", bookingTitle: "", bookingWhen: "", bookingA11y: "",
|
|
126
|
+
hasPlan: false, noPlan: true, planName: "", planDetail: "",
|
|
127
|
+
hasMessage: false, noMessage: true, messageTitle: "", messageBody: "",
|
|
128
|
+
errored: false, notErrored: true, ctaLabel: "", ctaScreenId: "", ctaMode: "", hideCta: true,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
function message(title: string, body: string): Partial<PlaceSummaryState> {
|
|
132
|
+
return { hasMessage: true, messageTitle: title, messageBody: body };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function read(arg: unknown, again = true): Promise<PlaceSummaryState> {
|
|
136
|
+
const input = (globalThis as { himalaya?: { input?: unknown } }).himalaya?.input;
|
|
137
|
+
const verticals = summaryVerticals(appearVerticals(arg), input);
|
|
138
|
+
const browse = verticals.bookings
|
|
139
|
+
? { ctaLabel: copyParam("placeSummary.browseCta", "Browse classes"), ctaScreenId: BOOKINGS_SCREEN }
|
|
140
|
+
: verticals.plans
|
|
141
|
+
? { ctaLabel: copyParam("placeSummary.plansCta", "See plans"), ctaScreenId: PLANS_SCREEN }
|
|
142
|
+
: { ctaLabel: "", ctaScreenId: "" };
|
|
143
|
+
const member: MemberAccess = await memberAccess();
|
|
144
|
+
if (!member.email) {
|
|
145
|
+
return derive({ ...BLANK, status: "ready",
|
|
146
|
+
...message(copyParam("placeSummary.guestTitle", "Sign in to see your bookings"),
|
|
147
|
+
copyParam("placeSummary.guestBody", "Your next class and plan show up here.")),
|
|
148
|
+
ctaLabel: copyParam("placeSummary.signInCta", "Sign in"), ctaScreenId: SIGN_IN_SCREEN, ctaMode: "signin" });
|
|
149
|
+
}
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
const [bookings, plans] = await Promise.allSettled([
|
|
152
|
+
verticals.bookings
|
|
153
|
+
? fetchMemberBookings({ query: "", filter: "value:Upcoming", sort: "default", savedIds: [] }, "", new Date(now).toISOString(), member)
|
|
154
|
+
: Promise.resolve({ items: [] as MemberBookingRow[] }),
|
|
155
|
+
verticals.plans ? listMyPlanOrders({ statuses: ["ACTIVE"], limit: 50 }) : Promise.resolve({ orders: [] as WixPlanOrder[] }),
|
|
156
|
+
]);
|
|
157
|
+
// The member changed mid-read (signed out, another account): nothing of the old one shows;
|
|
158
|
+
// read once more for whoever is signed in now.
|
|
159
|
+
try { await assertMemberAccess(member); } catch { return again ? read(arg, false) : derive({ ...BLANK, status: "ready" }); }
|
|
160
|
+
const booking = bookings.status === "fulfilled" ? nextBooking(bookings.value.items, now) : null;
|
|
161
|
+
// A 401/403 on the plan read is a member with no plan access, not a failure to show.
|
|
162
|
+
const plan = plans.status === "fulfilled" ? planLine(plans.value.orders ?? []) : null;
|
|
163
|
+
const bookingFailed = bookings.status === "rejected";
|
|
164
|
+
const planFailed = plans.status === "rejected" && !memberAccessDenied(plans.reason);
|
|
165
|
+
const base: PlaceSummaryState = { ...BLANK, status: "ready",
|
|
166
|
+
hasBooking: !!booking, bookingId: booking?.id ?? "", bookingDay: booking?.day ?? "",
|
|
167
|
+
bookingTitle: booking?.title ?? "", bookingWhen: booking?.subtitle ?? "",
|
|
168
|
+
bookingA11y: booking ? [copyParam("placeSummary.nextBooking", "Next booking"), booking.title, booking.subtitle].filter(Boolean).join(", ") : "",
|
|
169
|
+
hasPlan: !!plan, planName: plan?.name ?? "", planDetail: plan?.detail ?? "" };
|
|
170
|
+
if (!booking && !plan && (bookingFailed || planFailed)) {
|
|
171
|
+
return derive({ ...base, status: "error", errored: true,
|
|
172
|
+
...message(copyParam("placeSummary.errorTitle", "Couldn't load your summary"),
|
|
173
|
+
copyParam("placeSummary.errorBody", "We couldn't reach the site. Try again in a moment.")) });
|
|
174
|
+
}
|
|
175
|
+
if (!booking && !plan) {
|
|
176
|
+
return derive({ ...base, ...browse,
|
|
177
|
+
...message(copyParam("placeSummary.emptyTitle", "Nothing booked yet"),
|
|
178
|
+
verticals.bookings
|
|
179
|
+
? copyParam("placeSummary.emptyBody", "Find a class that fits your week.")
|
|
180
|
+
: copyParam("placeSummary.emptyPlansBody", "Pick a plan to get started.")) });
|
|
181
|
+
}
|
|
182
|
+
return derive({ ...base,
|
|
183
|
+
...(verticals.bookings
|
|
184
|
+
? { ctaLabel: copyParam("placeSummary.bookCta", "Book"), ctaScreenId: BOOKINGS_SCREEN }
|
|
185
|
+
: browse) });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** One read at a time wins: a refresh that lands while an older load is still out replaces it,
|
|
189
|
+
* and the older one's answer is dropped — `undefined`, not its starting state, which would
|
|
190
|
+
* write back over the newer result. */
|
|
191
|
+
async function load(_s: PlaceSummaryState, arg?: unknown): Promise<PlaceSummaryState | undefined> {
|
|
192
|
+
stashCopy(arg);
|
|
193
|
+
const mine = ++generation;
|
|
194
|
+
const next = await read(arg);
|
|
195
|
+
return mine === generation ? next : undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const svc = defineService<PlaceSummaryState, {
|
|
199
|
+
load: (s: PlaceSummaryState, arg?: unknown) => Promise<PlaceSummaryState | undefined>;
|
|
200
|
+
retry: (s: PlaceSummaryState) => Promise<PlaceSummaryState | undefined>;
|
|
201
|
+
}>("himiPlaceSummary", {
|
|
202
|
+
state: BLANK,
|
|
203
|
+
actions: {
|
|
204
|
+
load,
|
|
205
|
+
retry: (s) => load(s),
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
export { svc };
|
|
@@ -112,6 +112,7 @@ export const HOSTED_SCREEN_VERTICALS: Readonly<Record<string, readonly BrandedLi
|
|
|
112
112
|
himi_notifications: ["notifications"],
|
|
113
113
|
himi_orders: ["store"],
|
|
114
114
|
himi_photo_album: ["galleries"],
|
|
115
|
+
himi_place_summary: ["bookings", "plans"],
|
|
115
116
|
himi_plan: ["plans"],
|
|
116
117
|
himi_plans: ["plans"],
|
|
117
118
|
himi_podcast: ["podcasts"],
|
|
@@ -99,6 +99,24 @@ export function serviceIsChargeable(service: unknown): boolean {
|
|
|
99
99
|
return amount != null && amount > 0;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Format one Bookings v2 Money value (`{ value, currency }`, `value` a decimal string) — a
|
|
104
|
+
* price, a deposit, a VARIED minimum, a cancellation fee. A whole amount drops its cents
|
|
105
|
+
* ("₪90", not "₪90.00"), as the service screen shows it. "" when it carries no usable
|
|
106
|
+
* amount. The currency comes from the Money itself, then from `fallbackCurrency`.
|
|
107
|
+
*/
|
|
108
|
+
export function formatServiceMoney(money: unknown, fallbackCurrency = "USD"): string {
|
|
109
|
+
const m = (money ?? {}) as { value?: unknown; amount?: unknown; currency?: unknown };
|
|
110
|
+
const amount = numberOf(m.value ?? m.amount);
|
|
111
|
+
if (amount == null) return "";
|
|
112
|
+
const currency = stringOf(m.currency) || fallbackCurrency;
|
|
113
|
+
try {
|
|
114
|
+
return formatCurrency(amount, currency, "en", Number.isInteger(amount) ? 0 : undefined);
|
|
115
|
+
} catch {
|
|
116
|
+
return `${amount}`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
102
120
|
/**
|
|
103
121
|
* Format a Wix Bookings service's price for display, e.g. "₪111" / "$20".
|
|
104
122
|
*
|
|
@@ -112,6 +112,7 @@ export const HOSTED_SCREEN_VERTICALS: Readonly<Record<string, readonly BrandedLi
|
|
|
112
112
|
himi_notifications: ["notifications"],
|
|
113
113
|
himi_orders: ["store"],
|
|
114
114
|
himi_photo_album: ["galleries"],
|
|
115
|
+
himi_place_summary: ["bookings", "plans"],
|
|
115
116
|
himi_plan: ["plans"],
|
|
116
117
|
himi_plans: ["plans"],
|
|
117
118
|
himi_podcast: ["podcasts"],
|
|
@@ -99,6 +99,24 @@ export function serviceIsChargeable(service: unknown): boolean {
|
|
|
99
99
|
return amount != null && amount > 0;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Format one Bookings v2 Money value (`{ value, currency }`, `value` a decimal string) — a
|
|
104
|
+
* price, a deposit, a VARIED minimum, a cancellation fee. A whole amount drops its cents
|
|
105
|
+
* ("₪90", not "₪90.00"), as the service screen shows it. "" when it carries no usable
|
|
106
|
+
* amount. The currency comes from the Money itself, then from `fallbackCurrency`.
|
|
107
|
+
*/
|
|
108
|
+
export function formatServiceMoney(money: unknown, fallbackCurrency = "USD"): string {
|
|
109
|
+
const m = (money ?? {}) as { value?: unknown; amount?: unknown; currency?: unknown };
|
|
110
|
+
const amount = numberOf(m.value ?? m.amount);
|
|
111
|
+
if (amount == null) return "";
|
|
112
|
+
const currency = stringOf(m.currency) || fallbackCurrency;
|
|
113
|
+
try {
|
|
114
|
+
return formatCurrency(amount, currency, "en", Number.isInteger(amount) ? 0 : undefined);
|
|
115
|
+
} catch {
|
|
116
|
+
return `${amount}`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
102
120
|
/**
|
|
103
121
|
* Format a Wix Bookings service's price for display, e.g. "₪111" / "$20".
|
|
104
122
|
*
|
|
@@ -112,6 +112,7 @@ export const HOSTED_SCREEN_VERTICALS: Readonly<Record<string, readonly BrandedLi
|
|
|
112
112
|
himi_notifications: ["notifications"],
|
|
113
113
|
himi_orders: ["store"],
|
|
114
114
|
himi_photo_album: ["galleries"],
|
|
115
|
+
himi_place_summary: ["bookings", "plans"],
|
|
115
116
|
himi_plan: ["plans"],
|
|
116
117
|
himi_plans: ["plans"],
|
|
117
118
|
himi_podcast: ["podcasts"],
|
|
@@ -99,6 +99,24 @@ export function serviceIsChargeable(service: unknown): boolean {
|
|
|
99
99
|
return amount != null && amount > 0;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Format one Bookings v2 Money value (`{ value, currency }`, `value` a decimal string) — a
|
|
104
|
+
* price, a deposit, a VARIED minimum, a cancellation fee. A whole amount drops its cents
|
|
105
|
+
* ("₪90", not "₪90.00"), as the service screen shows it. "" when it carries no usable
|
|
106
|
+
* amount. The currency comes from the Money itself, then from `fallbackCurrency`.
|
|
107
|
+
*/
|
|
108
|
+
export function formatServiceMoney(money: unknown, fallbackCurrency = "USD"): string {
|
|
109
|
+
const m = (money ?? {}) as { value?: unknown; amount?: unknown; currency?: unknown };
|
|
110
|
+
const amount = numberOf(m.value ?? m.amount);
|
|
111
|
+
if (amount == null) return "";
|
|
112
|
+
const currency = stringOf(m.currency) || fallbackCurrency;
|
|
113
|
+
try {
|
|
114
|
+
return formatCurrency(amount, currency, "en", Number.isInteger(amount) ? 0 : undefined);
|
|
115
|
+
} catch {
|
|
116
|
+
return `${amount}`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
102
120
|
/**
|
|
103
121
|
* Format a Wix Bookings service's price for display, e.g. "₪111" / "$20".
|
|
104
122
|
*
|