@wix/himalaya-cli 0.1328.0 → 0.1329.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/cli.mjs +2 -3
- package/package.json +2 -2
- package/templates/beauty-salon/config.ts +1 -1
- package/templates/beauty-salon/tier5-src/_shared/branded-lite-hosted.ts +271 -0
- package/templates/beauty-salon/tier5-src/_shared/home-service.ts +3 -2
- package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +32 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/home-service.ts +3 -2
- package/templates/branded-lite/config.ts +1 -1
- package/templates/branded-lite/tier5-src/_shared/branded-lite-hosted.ts +271 -0
- package/templates/branded-lite/tier5-src/_shared/home-service.ts +3 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +32 -0
- package/templates/community-club/config.ts +1 -1
- package/templates/community-club/tier5-src/_shared/branded-lite-hosted.ts +271 -0
- package/templates/community-club/tier5-src/_shared/home-service.ts +3 -2
- package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +32 -0
- package/templates/community-club-owner/tier5-src/_shared/home-service.ts +3 -2
- package/templates/events-meetups/config.ts +1 -1
- package/templates/events-meetups/tier5-src/_shared/branded-lite-hosted.ts +271 -0
- package/templates/events-meetups/tier5-src/_shared/home-service.ts +3 -2
- package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +32 -0
- package/templates/events-meetups-owner/tier5-src/_shared/home-service.ts +3 -2
- package/templates/gym-fitness/config.ts +1 -1
- package/templates/gym-fitness/tier5-src/_shared/branded-lite-hosted.ts +271 -0
- package/templates/gym-fitness/tier5-src/_shared/home-service.ts +3 -2
- package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +32 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/home-service.ts +3 -2
- package/templates/nonprofit-cause/config.ts +1 -1
- package/templates/nonprofit-cause/tier5-src/_shared/branded-lite-hosted.ts +271 -0
- package/templates/nonprofit-cause/tier5-src/_shared/home-service.ts +3 -2
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +32 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/home-service.ts +3 -2
- package/templates/owner-lite/tier5-src/_shared/home-service.ts +3 -2
- package/templates/podcast-creator/config.ts +1 -1
- package/templates/podcast-creator/tier5-src/_shared/branded-lite-hosted.ts +271 -0
- package/templates/podcast-creator/tier5-src/_shared/home-service.ts +3 -2
- package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +32 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/home-service.ts +3 -2
- package/templates/portfolio-creative/config.ts +1 -1
- package/templates/portfolio-creative/tier5-src/_shared/branded-lite-hosted.ts +271 -0
- package/templates/portfolio-creative/tier5-src/_shared/home-service.ts +3 -2
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +32 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/home-service.ts +3 -2
- package/templates/restaurant/config.ts +1 -1
- package/templates/restaurant/tier5-src/_shared/branded-lite-hosted.ts +271 -0
- package/templates/restaurant/tier5-src/_shared/home-service.ts +3 -2
- package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +32 -0
- package/templates/restaurant-owner/tier5-src/_shared/home-service.ts +3 -2
- package/templates/retail-store/config.ts +1 -1
- package/templates/retail-store/tier5-src/_shared/branded-lite-hosted.ts +271 -0
- package/templates/retail-store/tier5-src/_shared/home-service.ts +3 -2
- package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +32 -0
- package/templates/retail-store-owner/tier5-src/_shared/home-service.ts +3 -2
- package/templates/yoga-studio/config.ts +1 -1
- package/templates/yoga-studio/tier5-src/_shared/branded-lite-hosted.ts +271 -0
- package/templates/yoga-studio/tier5-src/_shared/home-service.ts +3 -2
- package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +32 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/home-service.ts +3 -2
|
@@ -6,7 +6,7 @@ type State = Record<string, unknown>;
|
|
|
6
6
|
type Action = (s: State, ...args: unknown[]) => State | undefined | Promise<State | undefined>;
|
|
7
7
|
const record = (x: unknown): State => x && typeof x === 'object' && !Array.isArray(x) ? x as State : {};
|
|
8
8
|
|
|
9
|
-
export function decorateHome(def: { state: State; actions?: Record<string, Action
|
|
9
|
+
export function decorateHome(def: { state: State; actions?: Record<string, Action>; notHydrated?: readonly string[] }) {
|
|
10
10
|
let modules: HomeModule[] = [];
|
|
11
11
|
const persisted = localPreferences<Partial<HomePreferences>>('home', value => {
|
|
12
12
|
const v = record(value);
|
|
@@ -85,7 +85,8 @@ export function decorateHome(def: { state: State; actions?: Record<string, Actio
|
|
|
85
85
|
actions.homePin = change(id => { if (modules.some(m => m.id === id)) { prefs.pinned = prefs.pinned.includes(id) ? prefs.pinned.filter(x => x !== id) : [...prefs.pinned, id]; prefs.hidden = prefs.hidden.filter(x => x !== id); } });
|
|
86
86
|
actions.homeMoveUp = change(id => { const index = prefs.order.indexOf(id); if (index > 0) [prefs.order[index - 1], prefs.order[index]] = [prefs.order[index], prefs.order[index - 1]]; });
|
|
87
87
|
actions.homeMoveDown = change(id => { const index = prefs.order.indexOf(id); if (index >= 0 && index < prefs.order.length - 1) [prefs.order[index], prefs.order[index + 1]] = [prefs.order[index + 1], prefs.order[index]]; });
|
|
88
|
-
|
|
88
|
+
// Spread first: the rest of the definition (`notHydrated`) must reach the runtime too.
|
|
89
|
+
return { ...def, state: derive(def.state), actions };
|
|
89
90
|
}
|
|
90
91
|
export const defineService: typeof register = ((name: string, def: ServiceDefinition<unknown>) =>
|
|
91
92
|
register(name, { ...def, ...decorateHome(def as unknown as { state: State; actions?: Record<string, Action> }) })) as typeof register;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fetchMemberBookings, type MemberBookingRow } from "../_shared/member-bookings.js";
|
|
2
|
+
import { hostSiteFromInput, hostedExploreCards, hostedHiddenFlags, type HostedExploreCard } from "../_shared/branded-lite-hosted.js";
|
|
2
3
|
import { memberAccess, assertMemberAccess } from "../_shared/member-access.js";
|
|
3
4
|
import { defineService as registerHome } from "../_shared/home-service.js";
|
|
4
5
|
import { withIapPresentation } from "../_shared/iap-presentation.js";
|
|
@@ -191,6 +192,14 @@ interface GroupCard {
|
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
interface State {
|
|
195
|
+
/** Hosted mode (#4089 B2 Task 2c): per vertical (and drawer folder), whether it is hidden for
|
|
196
|
+
* the site the host opened this app for. The hosted home binds it fail-closed; a single-site
|
|
197
|
+
* build never reads it. */
|
|
198
|
+
hosted: Record<string, boolean>;
|
|
199
|
+
/** The host's site name, "" outside hosted mode. */
|
|
200
|
+
hostedName: string;
|
|
201
|
+
/** The hosted Home's Explore cards: the site's verticals only, [] without a host. */
|
|
202
|
+
hostedCards: HostedExploreCard[];
|
|
194
203
|
nextBooking: MemberBookingRow[];
|
|
195
204
|
hideNextBooking: boolean;
|
|
196
205
|
memberProgressLabel: string;
|
|
@@ -864,8 +873,20 @@ function derive(state: State): State {
|
|
|
864
873
|
};
|
|
865
874
|
}
|
|
866
875
|
|
|
876
|
+
/** The site the host opened this app for, or null (single-site build, or no host). */
|
|
877
|
+
const workerInput = (globalThis as { himalaya?: { input?: { hostedExplore?: unknown } } }).himalaya?.input;
|
|
878
|
+
const hostedSite = hostSiteFromInput(workerInput);
|
|
879
|
+
|
|
880
|
+
/** This boot's hosted fields — never a snapshot's (see `notHydrated` and the reset below). */
|
|
881
|
+
const hostedBoot = {
|
|
882
|
+
hosted: hostedHiddenFlags(hostedSite),
|
|
883
|
+
hostedName: hostedSite?.name ?? "",
|
|
884
|
+
hostedCards: hostedExploreCards(workerInput?.hostedExplore, hostedSite),
|
|
885
|
+
};
|
|
886
|
+
|
|
867
887
|
function baseState(): State {
|
|
868
888
|
return derive({
|
|
889
|
+
...hostedBoot,
|
|
869
890
|
nextBooking: [], hideNextBooking: true, memberProgressLabel: "", memberProgressFraction: 0, hideMemberProgress: true,
|
|
870
891
|
personalError: false, hidePersonalError: true,
|
|
871
892
|
savedProducts: [], noSavedProducts: true, savedPosts: [], noSavedPosts: true, noSavedContent: true,
|
|
@@ -1136,6 +1157,9 @@ const svc = defineService<State, {
|
|
|
1136
1157
|
personalLoaded: (state: State, arg: { generation: number; rows: MemberBookingRow[]; progress: { label: string; fraction: number }; error: boolean; weekly: { count: number; fraction: number; label: string } | null }) => State;
|
|
1137
1158
|
}>("himiBrandedHome", {
|
|
1138
1159
|
state: baseState(),
|
|
1160
|
+
// The host's site is read fresh at every boot: a snapshot of the last session's site must not
|
|
1161
|
+
// decide this one's verticals or name (an owner installs or removes one; another site opens).
|
|
1162
|
+
notHydrated: ["hosted", "hostedName", "hostedCards"],
|
|
1139
1163
|
actions: {
|
|
1140
1164
|
clearPersonal: state => derive({ ...state, nextBooking: [], hideNextBooking: true, memberProgressLabel: "", memberProgressFraction: 0, hideMemberProgress: true, personalError: false, hidePersonalError: true,
|
|
1141
1165
|
weeklyClassCount: 0, weeklyClassGoalFraction: 0, weeklyGoalLabel: "" }),
|
|
@@ -1154,5 +1178,13 @@ const svc = defineService<State, {
|
|
|
1154
1178
|
},
|
|
1155
1179
|
});
|
|
1156
1180
|
|
|
1181
|
+
// A runtime that predates `notHydrated` has already laid the snapshot over the state: put this boot's
|
|
1182
|
+
// hosted fields back in place before any action runs, so site B never opens with site A's verticals or
|
|
1183
|
+
// name. In place, because on those runtimes getState() IS the stored state (as himi-player-home does).
|
|
1184
|
+
if ((globalThis as { himalaya?: { hydrationOptOut?: unknown } }).himalaya?.hydrationOptOut !== true) {
|
|
1185
|
+
const live = (svc as { getState?: () => State | undefined }).getState?.();
|
|
1186
|
+
if (live && typeof live === "object") Object.assign(live, hostedBoot);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1157
1189
|
svc.refresh();
|
|
1158
1190
|
export { svc };
|
|
@@ -6,7 +6,7 @@ type State = Record<string, unknown>;
|
|
|
6
6
|
type Action = (s: State, ...args: unknown[]) => State | undefined | Promise<State | undefined>;
|
|
7
7
|
const record = (x: unknown): State => x && typeof x === 'object' && !Array.isArray(x) ? x as State : {};
|
|
8
8
|
|
|
9
|
-
export function decorateHome(def: { state: State; actions?: Record<string, Action
|
|
9
|
+
export function decorateHome(def: { state: State; actions?: Record<string, Action>; notHydrated?: readonly string[] }) {
|
|
10
10
|
let modules: HomeModule[] = [];
|
|
11
11
|
const persisted = localPreferences<Partial<HomePreferences>>('home', value => {
|
|
12
12
|
const v = record(value);
|
|
@@ -85,7 +85,8 @@ export function decorateHome(def: { state: State; actions?: Record<string, Actio
|
|
|
85
85
|
actions.homePin = change(id => { if (modules.some(m => m.id === id)) { prefs.pinned = prefs.pinned.includes(id) ? prefs.pinned.filter(x => x !== id) : [...prefs.pinned, id]; prefs.hidden = prefs.hidden.filter(x => x !== id); } });
|
|
86
86
|
actions.homeMoveUp = change(id => { const index = prefs.order.indexOf(id); if (index > 0) [prefs.order[index - 1], prefs.order[index]] = [prefs.order[index], prefs.order[index - 1]]; });
|
|
87
87
|
actions.homeMoveDown = change(id => { const index = prefs.order.indexOf(id); if (index >= 0 && index < prefs.order.length - 1) [prefs.order[index], prefs.order[index + 1]] = [prefs.order[index + 1], prefs.order[index]]; });
|
|
88
|
-
|
|
88
|
+
// Spread first: the rest of the definition (`notHydrated`) must reach the runtime too.
|
|
89
|
+
return { ...def, state: derive(def.state), actions };
|
|
89
90
|
}
|
|
90
91
|
export const defineService: typeof register = ((name: string, def: ServiceDefinition<unknown>) =>
|
|
91
92
|
register(name, { ...def, ...decorateHome(def as unknown as { state: State; actions?: Record<string, Action> }) })) as typeof register;
|
|
@@ -6,7 +6,7 @@ type State = Record<string, unknown>;
|
|
|
6
6
|
type Action = (s: State, ...args: unknown[]) => State | undefined | Promise<State | undefined>;
|
|
7
7
|
const record = (x: unknown): State => x && typeof x === 'object' && !Array.isArray(x) ? x as State : {};
|
|
8
8
|
|
|
9
|
-
export function decorateHome(def: { state: State; actions?: Record<string, Action
|
|
9
|
+
export function decorateHome(def: { state: State; actions?: Record<string, Action>; notHydrated?: readonly string[] }) {
|
|
10
10
|
let modules: HomeModule[] = [];
|
|
11
11
|
const persisted = localPreferences<Partial<HomePreferences>>('home', value => {
|
|
12
12
|
const v = record(value);
|
|
@@ -85,7 +85,8 @@ export function decorateHome(def: { state: State; actions?: Record<string, Actio
|
|
|
85
85
|
actions.homePin = change(id => { if (modules.some(m => m.id === id)) { prefs.pinned = prefs.pinned.includes(id) ? prefs.pinned.filter(x => x !== id) : [...prefs.pinned, id]; prefs.hidden = prefs.hidden.filter(x => x !== id); } });
|
|
86
86
|
actions.homeMoveUp = change(id => { const index = prefs.order.indexOf(id); if (index > 0) [prefs.order[index - 1], prefs.order[index]] = [prefs.order[index], prefs.order[index - 1]]; });
|
|
87
87
|
actions.homeMoveDown = change(id => { const index = prefs.order.indexOf(id); if (index >= 0 && index < prefs.order.length - 1) [prefs.order[index], prefs.order[index + 1]] = [prefs.order[index + 1], prefs.order[index]]; });
|
|
88
|
-
|
|
88
|
+
// Spread first: the rest of the definition (`notHydrated`) must reach the runtime too.
|
|
89
|
+
return { ...def, state: derive(def.state), actions };
|
|
89
90
|
}
|
|
90
91
|
export const defineService: typeof register = ((name: string, def: ServiceDefinition<unknown>) =>
|
|
91
92
|
register(name, { ...def, ...decorateHome(def as unknown as { state: State; actions?: Record<string, Action> }) })) as typeof register;
|
|
@@ -37,7 +37,7 @@ export default defineApp({
|
|
|
37
37
|
actions: "http://localhost:8190/v1/actions",
|
|
38
38
|
sync: "http://localhost:8190/v1/sync",
|
|
39
39
|
},
|
|
40
|
-
capabilities: ["live_activity","push","share","secure_storage","open_url","calendar","local_database","media_playback","wix_api","wix_platform","app_info","iap","release_control","clipboard"],
|
|
40
|
+
capabilities: ["live_activity","push","share","secure_storage","open_url","calendar","local_database","media_playback","wix_api","wix_platform","app_info","iap","release_control","clipboard","theme"],
|
|
41
41
|
designTokensPath: "./tokens.json",
|
|
42
42
|
navigation: {
|
|
43
43
|
"navKind": "responsive",
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// Hosted mode: ONE served Branded Lite that opens any site at runtime, inside a host app such as
|
|
2
|
+
// Spaces Lite, instead of one build per customer.
|
|
3
|
+
//
|
|
4
|
+
// A single-site build decides its verticals at build time from `provisioning.json`. A hosted build
|
|
5
|
+
// declares every vertical and decides at RUNTIME from the site the host opened it for: each
|
|
6
|
+
// vertical's nav row carries a fail-closed `hiddenPath` (`hiddenUntilResolved`: hidden until it resolves), and
|
|
7
|
+
// the app-level nav worker resolves it from the site the host hands over.
|
|
8
|
+
//
|
|
9
|
+
// WHERE THE SITE COMES FROM: the host. A hosted app's Wix capability is pinned to its site and only
|
|
10
|
+
// reaches www.wixapis.com, so it cannot read its own provisioning (the Member View
|
|
11
|
+
// lives on apps.wix.com, the display options on manage.wix.com). The host resolves it with its own
|
|
12
|
+
// account session (the site provisioning mapper) and core injects it into every worker's input under the
|
|
13
|
+
// reserved key `HOST_SITE_INPUT_KEY`, after the nav params are merged — so a deep link's query can
|
|
14
|
+
// never supply or replace it (WorkerPreferenceContext.inject, iOS + Android).
|
|
15
|
+
//
|
|
16
|
+
// PURE and import-free, so the nav and home workers (and the CLI templates that copy them) can
|
|
17
|
+
// bundle it. The app's hosted-mode module re-exports it with the app's types.
|
|
18
|
+
|
|
19
|
+
/** A Branded Lite vertical id (the app contract's `BrandedLiteVertical`). A plain
|
|
20
|
+
* string here: this module is copied into CLI templates, which only allow `_shared` imports. */
|
|
21
|
+
export type BrandedLiteVertical = string;
|
|
22
|
+
|
|
23
|
+
export type BrandedLiteMode = "single-site" | "hosted";
|
|
24
|
+
|
|
25
|
+
/** The worker-input key the host's site arrives under. Core owns it: it is set (or blanked) after
|
|
26
|
+
* every other input source, so nothing but the host can put a site there. */
|
|
27
|
+
export const HOST_SITE_INPUT_KEY = "__himiHostSite";
|
|
28
|
+
|
|
29
|
+
/** The host's site from a worker's `himalaya.input`, or null. */
|
|
30
|
+
export function hostSiteFromInput(input: unknown): HostedSite | null {
|
|
31
|
+
const raw = input && typeof input === "object" ? (input as Record<string, unknown>)[HOST_SITE_INPUT_KEY] : undefined;
|
|
32
|
+
return readHostedSite(raw);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** What the host hands a hosted Branded Lite about the site it opened (`himalaya.input[HOST_SITE_INPUT_KEY]`). */
|
|
36
|
+
export interface HostedSite {
|
|
37
|
+
metaSiteId: string;
|
|
38
|
+
name: string;
|
|
39
|
+
installedVerticals: BrandedLiteVertical[];
|
|
40
|
+
/** #RRGGBB, or "" when the site has none (the build's own brand stays). */
|
|
41
|
+
primaryColor: string;
|
|
42
|
+
logoUrl: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Verticals a hosted build always shows: every member area has a member list, whatever the
|
|
46
|
+
* owner installed. (Home, its About section and Profile are not verticals — always there.) */
|
|
47
|
+
export const HOSTED_ALWAYS: readonly BrandedLiteVertical[] = ["members"];
|
|
48
|
+
|
|
49
|
+
/** The screen each vertical's nav rows open, so a route to an uninstalled vertical can be answered. */
|
|
50
|
+
export const VERTICAL_ROOT_SCREENS: Readonly<Record<string, readonly string[]>> = {
|
|
51
|
+
store: ["himi_shop"], bookings: ["himi_services"], restaurants: ["himi_menu", "himi_reserve"],
|
|
52
|
+
programs: ["himi_programs"], plans: ["himi_plans"], feed: ["himi_feed"], members: ["himi_members"],
|
|
53
|
+
forum: ["himi_forum"], coupons: ["himi_coupons"], notifications: ["himi_notifications"],
|
|
54
|
+
loyalty: ["himi_loyalty"], reviews: ["himi_reviews"], portfolio: ["himi_portfolio"],
|
|
55
|
+
reservations: ["himi_reservations"], invoices: ["himi_invoices"], badges: ["himi_badges"],
|
|
56
|
+
notifsettings: ["himi_notif_settings"], events: ["himi_events"], blog: ["himi_blog"],
|
|
57
|
+
groups: ["himi_groups"], cms: ["himi_cms"], donations: ["himi_donations"], giftCards: ["himi_gift_cards"],
|
|
58
|
+
videos: ["himi_videos"], podcasts: ["himi_podcasts"], forms: ["himi_forms"], galleries: ["himi_galleries"],
|
|
59
|
+
inbox: ["himi_inbox"], faq: ["himi_faq"],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Every screen a vertical owns, so a route to ANY of them (a child or action screen as much as the
|
|
63
|
+
* root) lands on `NOT_AVAILABLE_SCREEN` when the site does not have it. A screen several verticals
|
|
64
|
+
* share opens when any of them is shown. Pinned against the app's screen registry
|
|
65
|
+
* (`brandedLiteScreenOwners`, checked by the app's hosted-mode tests), so a screen added
|
|
66
|
+
* to a vertical without being listed here fails the build. */
|
|
67
|
+
export const HOSTED_SCREEN_VERTICALS: Readonly<Record<string, readonly BrandedLiteVertical[]>> = {
|
|
68
|
+
himi_badge: ["badges"],
|
|
69
|
+
himi_badges: ["badges"],
|
|
70
|
+
himi_blog: ["blog"],
|
|
71
|
+
himi_bookings: ["bookings"],
|
|
72
|
+
himi_cart: ["store"],
|
|
73
|
+
himi_chat_thread: ["inbox", "members"],
|
|
74
|
+
himi_cms: ["cms"],
|
|
75
|
+
himi_collection: ["store"],
|
|
76
|
+
himi_collections: ["store"],
|
|
77
|
+
himi_coupon: ["coupons"],
|
|
78
|
+
himi_coupons: ["coupons"],
|
|
79
|
+
himi_create_post: ["blog", "forum"],
|
|
80
|
+
himi_donation: ["donations"],
|
|
81
|
+
himi_donations: ["donations"],
|
|
82
|
+
himi_events: ["events"],
|
|
83
|
+
himi_faq: ["faq"],
|
|
84
|
+
himi_feed: ["feed"],
|
|
85
|
+
himi_food_order: ["restaurants"],
|
|
86
|
+
himi_food_orders: ["restaurants"],
|
|
87
|
+
himi_form: ["forms"],
|
|
88
|
+
himi_forms: ["forms"],
|
|
89
|
+
himi_forum: ["forum"],
|
|
90
|
+
himi_forum_post: ["forum"],
|
|
91
|
+
himi_galleries: ["galleries"],
|
|
92
|
+
himi_gallery: ["galleries"],
|
|
93
|
+
himi_gift_card: ["giftCards"],
|
|
94
|
+
himi_gift_cards: ["giftCards"],
|
|
95
|
+
himi_groups: ["groups"],
|
|
96
|
+
himi_inbox: ["inbox"],
|
|
97
|
+
himi_invoice: ["invoices"],
|
|
98
|
+
himi_invoices: ["invoices"],
|
|
99
|
+
himi_loyalty: ["loyalty"],
|
|
100
|
+
himi_loyalty_reward: ["loyalty"],
|
|
101
|
+
himi_member: ["members"],
|
|
102
|
+
himi_members: ["members"],
|
|
103
|
+
himi_menu: ["restaurants"],
|
|
104
|
+
himi_my_events: ["events"],
|
|
105
|
+
himi_my_subscriptions: ["plans"],
|
|
106
|
+
himi_new_message: ["inbox", "members"],
|
|
107
|
+
himi_notif_settings: ["notifsettings"],
|
|
108
|
+
himi_notifications: ["notifications"],
|
|
109
|
+
himi_orders: ["store"],
|
|
110
|
+
himi_photo_album: ["galleries"],
|
|
111
|
+
himi_plan: ["plans"],
|
|
112
|
+
himi_plans: ["plans"],
|
|
113
|
+
himi_podcast: ["podcasts"],
|
|
114
|
+
himi_podcasts: ["podcasts"],
|
|
115
|
+
himi_portfolio: ["portfolio"],
|
|
116
|
+
himi_portfolio_project: ["portfolio"],
|
|
117
|
+
himi_programs: ["programs"],
|
|
118
|
+
himi_reservations: ["reservations"],
|
|
119
|
+
himi_reserve: ["restaurants"],
|
|
120
|
+
himi_review: ["reviews"],
|
|
121
|
+
himi_reviews: ["reviews"],
|
|
122
|
+
himi_saved: ["blog"],
|
|
123
|
+
himi_saved_products: ["store"],
|
|
124
|
+
himi_services: ["bookings"],
|
|
125
|
+
himi_shop: ["store"],
|
|
126
|
+
himi_subscription: ["plans"],
|
|
127
|
+
himi_video: ["videos"],
|
|
128
|
+
himi_video_channel: ["videos"],
|
|
129
|
+
himi_videos: ["videos"],
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/** The templated (`<prefix>[_/]<id>`) detail routes each vertical owns, pinned the same way. */
|
|
133
|
+
export const HOSTED_ROUTE_PREFIXES: Readonly<Record<string, readonly BrandedLiteVertical[]>> = {
|
|
134
|
+
himi_order: ["store"],
|
|
135
|
+
himi_product: ["store"],
|
|
136
|
+
himi_service: ["bookings"],
|
|
137
|
+
himi_time: ["bookings"],
|
|
138
|
+
himi_menu_item: ["restaurants"],
|
|
139
|
+
himi_program: ["programs"],
|
|
140
|
+
himi_event: ["events"],
|
|
141
|
+
himi_rsvp: ["events"],
|
|
142
|
+
himi_blog_post: ["blog"],
|
|
143
|
+
himi_group: ["groups"],
|
|
144
|
+
himi_cms_collection: ["cms"],
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/** The verticals that own [screenId], or undefined for a screen every site has (Home, Profile…). */
|
|
148
|
+
export function screenVerticals(screenId: string): readonly BrandedLiteVertical[] | undefined {
|
|
149
|
+
const exact = HOSTED_SCREEN_VERTICALS[screenId];
|
|
150
|
+
if (exact) return exact;
|
|
151
|
+
// The registry's `^<prefix>(?:[_/](.+))?$`: the bare prefix, or the prefix, a separator and an id.
|
|
152
|
+
for (const [prefix, verticals] of Object.entries(HOSTED_ROUTE_PREFIXES)) {
|
|
153
|
+
if (screenId === prefix) return verticals;
|
|
154
|
+
const sep = screenId.charAt(prefix.length);
|
|
155
|
+
if (screenId.startsWith(prefix) && (sep === "_" || sep === "/") && screenId.length > prefix.length + 1) return verticals;
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The drawer's collapsible folders and the verticals in each (config.ts builds them from this).
|
|
161
|
+
* A folder header is hidden when none of its verticals shows, so a site never sees an empty one. */
|
|
162
|
+
export const HOSTED_DRAWER_FOLDERS: Readonly<Record<string, readonly BrandedLiteVertical[]>> = {
|
|
163
|
+
drawer_sec_community: ["feed", "forum", "groups", "members", "reviews", "badges", "blog"],
|
|
164
|
+
drawer_sec_media: ["videos", "podcasts", "portfolio", "cms", "galleries", "faq"],
|
|
165
|
+
drawer_sec_offers: ["coupons", "giftCards", "donations", "loyalty"],
|
|
166
|
+
drawer_sec_activity: ["inbox", "reservations", "invoices", "forms", "notifications", "notifsettings"],
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/** The vertical a nav row's screen belongs to, or undefined (Home, Profile, Steps…). */
|
|
170
|
+
export function verticalOfScreen(screenId: string | undefined): BrandedLiteVertical | undefined {
|
|
171
|
+
if (!screenId) return undefined;
|
|
172
|
+
for (const [vertical, roots] of Object.entries(VERTICAL_ROOT_SCREENS) as [string, readonly string[]][]) {
|
|
173
|
+
if (roots.includes(screenId)) return vertical;
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The screen a route to an uninstalled vertical lands on. */
|
|
179
|
+
export const NOT_AVAILABLE_SCREEN = "himi_not_available";
|
|
180
|
+
|
|
181
|
+
const HEX = /^#[0-9A-Fa-f]{6}$/;
|
|
182
|
+
const VERTICALS = new Set(Object.keys(VERTICAL_ROOT_SCREENS));
|
|
183
|
+
|
|
184
|
+
/** The host's site, or null when it is missing or malformed — a hosted build then shows no
|
|
185
|
+
* vertical at all (fail closed), never every one. */
|
|
186
|
+
export function readHostedSite(raw: unknown): HostedSite | null {
|
|
187
|
+
let value = raw;
|
|
188
|
+
if (typeof value === "string") {
|
|
189
|
+
try { value = JSON.parse(value); } catch { return null; }
|
|
190
|
+
}
|
|
191
|
+
if (!value || typeof value !== "object") return null;
|
|
192
|
+
const v = value as Record<string, unknown>;
|
|
193
|
+
const metaSiteId = typeof v.metaSiteId === "string" ? v.metaSiteId.trim() : "";
|
|
194
|
+
if (!metaSiteId || !Array.isArray(v.installedVerticals)) return null;
|
|
195
|
+
const installed = v.installedVerticals.filter((x): x is BrandedLiteVertical => typeof x === "string" && VERTICALS.has(x));
|
|
196
|
+
const color = typeof v.primaryColor === "string" && HEX.test(v.primaryColor) ? v.primaryColor : "";
|
|
197
|
+
return {
|
|
198
|
+
metaSiteId,
|
|
199
|
+
name: typeof v.name === "string" ? v.name : "",
|
|
200
|
+
installedVerticals: [...new Set(installed)],
|
|
201
|
+
primaryColor: color,
|
|
202
|
+
logoUrl: typeof v.logoUrl === "string" && /^https:\/\//.test(v.logoUrl) ? v.logoUrl : "",
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The verticals a hosted build shows for [site]: its installed ones plus the always-on ones.
|
|
207
|
+
* None at all without a site. */
|
|
208
|
+
export function hostedVerticals(site: HostedSite | null): Set<BrandedLiteVertical> {
|
|
209
|
+
if (!site) return new Set();
|
|
210
|
+
return new Set<BrandedLiteVertical>([...site.installedVerticals, ...HOSTED_ALWAYS]);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** `hiddenPath` flags (true hides the row), for the nav worker to publish: one per vertical, and
|
|
214
|
+
* one per drawer folder (hidden when none of its verticals shows). */
|
|
215
|
+
export function hostedHiddenFlags(site: HostedSite | null): Record<string, boolean> {
|
|
216
|
+
const shown = hostedVerticals(site);
|
|
217
|
+
const out: Record<string, boolean> = {};
|
|
218
|
+
for (const v of Object.keys(VERTICAL_ROOT_SCREENS) as BrandedLiteVertical[]) out[v] = !shown.has(v);
|
|
219
|
+
for (const [folder, verticals] of Object.entries(HOSTED_DRAWER_FOLDERS)) out[folder] = !verticals.some((v) => shown.has(v));
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Where a route to [screenId] lands for [site]: itself, or the "not available on this site" screen
|
|
224
|
+
* when every vertical that owns it is missing from the site. A screen no vertical owns (Home,
|
|
225
|
+
* Profile, settings) is on every site and passes through. */
|
|
226
|
+
export function hostedRoute(screenId: string, site: HostedSite | null): string {
|
|
227
|
+
const owners = screenVerticals(screenId);
|
|
228
|
+
if (!owners) return screenId;
|
|
229
|
+
const shown = hostedVerticals(site);
|
|
230
|
+
return owners.some((v) => shown.has(v)) ? screenId : NOT_AVAILABLE_SCREEN;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** One hosted Home "Explore" card. The descriptor hands the worker the whole catalog (its copy is
|
|
234
|
+
* build-time localized) as `himalaya.input.hostedExplore`; the worker publishes only the site's. */
|
|
235
|
+
export interface HostedExploreCard {
|
|
236
|
+
vertical: BrandedLiteVertical;
|
|
237
|
+
icon: string;
|
|
238
|
+
title: string;
|
|
239
|
+
body: string;
|
|
240
|
+
/** Always the vertical's own root screen — never taken from the input. */
|
|
241
|
+
screenId: string;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** The Explore cards to show for [site], in catalog order: one per catalog entry whose vertical the
|
|
245
|
+
* site shows. Empty without a site or a catalog, so a worker that never boots shows no card at all
|
|
246
|
+
* (the grid binds this list — fail closed, unlike a per-card `hiddenPath`, which native renderers
|
|
247
|
+
* treat as visible until it resolves). */
|
|
248
|
+
export function hostedExploreCards(catalog: unknown, site: HostedSite | null): HostedExploreCard[] {
|
|
249
|
+
if (!Array.isArray(catalog)) return [];
|
|
250
|
+
const shown = hostedVerticals(site);
|
|
251
|
+
const out: HostedExploreCard[] = [];
|
|
252
|
+
const seen = new Set<string>();
|
|
253
|
+
for (const entry of catalog) {
|
|
254
|
+
if (!entry || typeof entry !== "object") continue;
|
|
255
|
+
const e = entry as Record<string, unknown>;
|
|
256
|
+
const vertical = typeof e.vertical === "string" ? e.vertical : "";
|
|
257
|
+
const screenId = VERTICAL_ROOT_SCREENS[vertical]?.[0];
|
|
258
|
+
if (!screenId || !shown.has(vertical) || seen.has(vertical)) continue;
|
|
259
|
+
seen.add(vertical);
|
|
260
|
+
out.push({
|
|
261
|
+
vertical, screenId,
|
|
262
|
+
icon: typeof e.icon === "string" ? e.icon : "grid",
|
|
263
|
+
title: typeof e.title === "string" ? e.title : "",
|
|
264
|
+
body: typeof e.body === "string" ? e.body : "",
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** The `$service` path a hosted nav row's visibility binds to (a vertical, or a drawer folder id). */
|
|
271
|
+
export const hostedHiddenPath = (key: string): string => `$service:brandedLiteNav.hosted.${key}`;
|
|
@@ -6,7 +6,7 @@ type State = Record<string, unknown>;
|
|
|
6
6
|
type Action = (s: State, ...args: unknown[]) => State | undefined | Promise<State | undefined>;
|
|
7
7
|
const record = (x: unknown): State => x && typeof x === 'object' && !Array.isArray(x) ? x as State : {};
|
|
8
8
|
|
|
9
|
-
export function decorateHome(def: { state: State; actions?: Record<string, Action
|
|
9
|
+
export function decorateHome(def: { state: State; actions?: Record<string, Action>; notHydrated?: readonly string[] }) {
|
|
10
10
|
let modules: HomeModule[] = [];
|
|
11
11
|
const persisted = localPreferences<Partial<HomePreferences>>('home', value => {
|
|
12
12
|
const v = record(value);
|
|
@@ -85,7 +85,8 @@ export function decorateHome(def: { state: State; actions?: Record<string, Actio
|
|
|
85
85
|
actions.homePin = change(id => { if (modules.some(m => m.id === id)) { prefs.pinned = prefs.pinned.includes(id) ? prefs.pinned.filter(x => x !== id) : [...prefs.pinned, id]; prefs.hidden = prefs.hidden.filter(x => x !== id); } });
|
|
86
86
|
actions.homeMoveUp = change(id => { const index = prefs.order.indexOf(id); if (index > 0) [prefs.order[index - 1], prefs.order[index]] = [prefs.order[index], prefs.order[index - 1]]; });
|
|
87
87
|
actions.homeMoveDown = change(id => { const index = prefs.order.indexOf(id); if (index >= 0 && index < prefs.order.length - 1) [prefs.order[index], prefs.order[index + 1]] = [prefs.order[index + 1], prefs.order[index]]; });
|
|
88
|
-
|
|
88
|
+
// Spread first: the rest of the definition (`notHydrated`) must reach the runtime too.
|
|
89
|
+
return { ...def, state: derive(def.state), actions };
|
|
89
90
|
}
|
|
90
91
|
export const defineService: typeof register = ((name: string, def: ServiceDefinition<unknown>) =>
|
|
91
92
|
register(name, { ...def, ...decorateHome(def as unknown as { state: State; actions?: Record<string, Action> }) })) as typeof register;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fetchMemberBookings, type MemberBookingRow } from "../_shared/member-bookings.js";
|
|
2
|
+
import { hostSiteFromInput, hostedExploreCards, hostedHiddenFlags, type HostedExploreCard } from "../_shared/branded-lite-hosted.js";
|
|
2
3
|
import { memberAccess, assertMemberAccess } from "../_shared/member-access.js";
|
|
3
4
|
import { defineService as registerHome } from "../_shared/home-service.js";
|
|
4
5
|
import { withIapPresentation } from "../_shared/iap-presentation.js";
|
|
@@ -191,6 +192,14 @@ interface GroupCard {
|
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
interface State {
|
|
195
|
+
/** Hosted mode (#4089 B2 Task 2c): per vertical (and drawer folder), whether it is hidden for
|
|
196
|
+
* the site the host opened this app for. The hosted home binds it fail-closed; a single-site
|
|
197
|
+
* build never reads it. */
|
|
198
|
+
hosted: Record<string, boolean>;
|
|
199
|
+
/** The host's site name, "" outside hosted mode. */
|
|
200
|
+
hostedName: string;
|
|
201
|
+
/** The hosted Home's Explore cards: the site's verticals only, [] without a host. */
|
|
202
|
+
hostedCards: HostedExploreCard[];
|
|
194
203
|
nextBooking: MemberBookingRow[];
|
|
195
204
|
hideNextBooking: boolean;
|
|
196
205
|
memberProgressLabel: string;
|
|
@@ -864,8 +873,20 @@ function derive(state: State): State {
|
|
|
864
873
|
};
|
|
865
874
|
}
|
|
866
875
|
|
|
876
|
+
/** The site the host opened this app for, or null (single-site build, or no host). */
|
|
877
|
+
const workerInput = (globalThis as { himalaya?: { input?: { hostedExplore?: unknown } } }).himalaya?.input;
|
|
878
|
+
const hostedSite = hostSiteFromInput(workerInput);
|
|
879
|
+
|
|
880
|
+
/** This boot's hosted fields — never a snapshot's (see `notHydrated` and the reset below). */
|
|
881
|
+
const hostedBoot = {
|
|
882
|
+
hosted: hostedHiddenFlags(hostedSite),
|
|
883
|
+
hostedName: hostedSite?.name ?? "",
|
|
884
|
+
hostedCards: hostedExploreCards(workerInput?.hostedExplore, hostedSite),
|
|
885
|
+
};
|
|
886
|
+
|
|
867
887
|
function baseState(): State {
|
|
868
888
|
return derive({
|
|
889
|
+
...hostedBoot,
|
|
869
890
|
nextBooking: [], hideNextBooking: true, memberProgressLabel: "", memberProgressFraction: 0, hideMemberProgress: true,
|
|
870
891
|
personalError: false, hidePersonalError: true,
|
|
871
892
|
savedProducts: [], noSavedProducts: true, savedPosts: [], noSavedPosts: true, noSavedContent: true,
|
|
@@ -1136,6 +1157,9 @@ const svc = defineService<State, {
|
|
|
1136
1157
|
personalLoaded: (state: State, arg: { generation: number; rows: MemberBookingRow[]; progress: { label: string; fraction: number }; error: boolean; weekly: { count: number; fraction: number; label: string } | null }) => State;
|
|
1137
1158
|
}>("himiBrandedHome", {
|
|
1138
1159
|
state: baseState(),
|
|
1160
|
+
// The host's site is read fresh at every boot: a snapshot of the last session's site must not
|
|
1161
|
+
// decide this one's verticals or name (an owner installs or removes one; another site opens).
|
|
1162
|
+
notHydrated: ["hosted", "hostedName", "hostedCards"],
|
|
1139
1163
|
actions: {
|
|
1140
1164
|
clearPersonal: state => derive({ ...state, nextBooking: [], hideNextBooking: true, memberProgressLabel: "", memberProgressFraction: 0, hideMemberProgress: true, personalError: false, hidePersonalError: true,
|
|
1141
1165
|
weeklyClassCount: 0, weeklyClassGoalFraction: 0, weeklyGoalLabel: "" }),
|
|
@@ -1154,5 +1178,13 @@ const svc = defineService<State, {
|
|
|
1154
1178
|
},
|
|
1155
1179
|
});
|
|
1156
1180
|
|
|
1181
|
+
// A runtime that predates `notHydrated` has already laid the snapshot over the state: put this boot's
|
|
1182
|
+
// hosted fields back in place before any action runs, so site B never opens with site A's verticals or
|
|
1183
|
+
// name. In place, because on those runtimes getState() IS the stored state (as himi-player-home does).
|
|
1184
|
+
if ((globalThis as { himalaya?: { hydrationOptOut?: unknown } }).himalaya?.hydrationOptOut !== true) {
|
|
1185
|
+
const live = (svc as { getState?: () => State | undefined }).getState?.();
|
|
1186
|
+
if (live && typeof live === "object") Object.assign(live, hostedBoot);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1157
1189
|
svc.refresh();
|
|
1158
1190
|
export { svc };
|
|
@@ -6,7 +6,7 @@ type State = Record<string, unknown>;
|
|
|
6
6
|
type Action = (s: State, ...args: unknown[]) => State | undefined | Promise<State | undefined>;
|
|
7
7
|
const record = (x: unknown): State => x && typeof x === 'object' && !Array.isArray(x) ? x as State : {};
|
|
8
8
|
|
|
9
|
-
export function decorateHome(def: { state: State; actions?: Record<string, Action
|
|
9
|
+
export function decorateHome(def: { state: State; actions?: Record<string, Action>; notHydrated?: readonly string[] }) {
|
|
10
10
|
let modules: HomeModule[] = [];
|
|
11
11
|
const persisted = localPreferences<Partial<HomePreferences>>('home', value => {
|
|
12
12
|
const v = record(value);
|
|
@@ -85,7 +85,8 @@ export function decorateHome(def: { state: State; actions?: Record<string, Actio
|
|
|
85
85
|
actions.homePin = change(id => { if (modules.some(m => m.id === id)) { prefs.pinned = prefs.pinned.includes(id) ? prefs.pinned.filter(x => x !== id) : [...prefs.pinned, id]; prefs.hidden = prefs.hidden.filter(x => x !== id); } });
|
|
86
86
|
actions.homeMoveUp = change(id => { const index = prefs.order.indexOf(id); if (index > 0) [prefs.order[index - 1], prefs.order[index]] = [prefs.order[index], prefs.order[index - 1]]; });
|
|
87
87
|
actions.homeMoveDown = change(id => { const index = prefs.order.indexOf(id); if (index >= 0 && index < prefs.order.length - 1) [prefs.order[index], prefs.order[index + 1]] = [prefs.order[index + 1], prefs.order[index]]; });
|
|
88
|
-
|
|
88
|
+
// Spread first: the rest of the definition (`notHydrated`) must reach the runtime too.
|
|
89
|
+
return { ...def, state: derive(def.state), actions };
|
|
89
90
|
}
|
|
90
91
|
export const defineService: typeof register = ((name: string, def: ServiceDefinition<unknown>) =>
|
|
91
92
|
register(name, { ...def, ...decorateHome(def as unknown as { state: State; actions?: Record<string, Action> }) })) as typeof register;
|
|
@@ -37,7 +37,7 @@ export default defineApp({
|
|
|
37
37
|
actions: "http://localhost:8190/v1/actions",
|
|
38
38
|
sync: "http://localhost:8190/v1/sync",
|
|
39
39
|
},
|
|
40
|
-
capabilities: ["live_activity","push","share","secure_storage","open_url","calendar","local_database","wix_api","wix_platform","app_info","iap","release_control","clipboard"],
|
|
40
|
+
capabilities: ["live_activity","push","share","secure_storage","open_url","calendar","local_database","wix_api","wix_platform","app_info","iap","release_control","clipboard","theme"],
|
|
41
41
|
designTokensPath: "./tokens.json",
|
|
42
42
|
navigation: {
|
|
43
43
|
"navKind": "responsive",
|