@wix/himalaya-cli 0.1613.0 → 0.1615.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/sdk/ui/src/index.d.ts +53 -13
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +3 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/ui.mjs +31 -5
- package/dist/cli.mjs +1638 -1330
- package/dist/preview-assets.generated.bin +0 -0
- package/fixtures/wix-canned.json +4 -0
- package/guides/component-reference.json +1 -1
- package/guides/component-reference.md +3 -0
- package/guides/designing-himi-screens.md +4 -1
- package/guides/hhp.md +2 -2
- package/guides/sdk-reference.json +28 -4
- package/package.json +2 -2
- package/template-icons/salon-spa.png +0 -0
- package/templates/beauty-salon/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/beauty-salon/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/beauty-salon/tier5-src/_shared/himi-booking-saved.ts +88 -0
- package/templates/beauty-salon/tier5-src/himi-booking-service/index.ts +50 -3
- package/templates/beauty-salon/tier5-src/himi-booking-services/index.ts +70 -3
- package/templates/beauty-salon/tier5-src/himi-booking-time/index.ts +40 -8
- package/templates/beauty-salon/tier5-src/himi-branded-lite-agenda/index.ts +7 -2
- package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +178 -12
- package/templates/branded-lite/dev/index.ts +192 -0
- package/templates/branded-lite/l10n/en.json +1 -1
- package/templates/branded-lite/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/branded-lite/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/branded-lite/tier5-src/_shared/himi-booking-saved.ts +88 -0
- package/templates/branded-lite/tier5-src/himi-booking-service/index.ts +50 -3
- package/templates/branded-lite/tier5-src/himi-booking-services/index.ts +70 -3
- package/templates/branded-lite/tier5-src/himi-booking-time/index.ts +40 -8
- package/templates/branded-lite/tier5-src/himi-branded-lite-agenda/index.ts +7 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-galleries/index.ts +104 -12
- package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +178 -12
- package/templates/community-club/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/community-club/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +178 -12
- package/templates/events-meetups/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/events-meetups/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +178 -12
- package/templates/gym-fitness/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/gym-fitness/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/gym-fitness/tier5-src/_shared/himi-booking-saved.ts +88 -0
- package/templates/gym-fitness/tier5-src/himi-booking-service/index.ts +50 -3
- package/templates/gym-fitness/tier5-src/himi-booking-services/index.ts +70 -3
- package/templates/gym-fitness/tier5-src/himi-booking-time/index.ts +40 -8
- package/templates/gym-fitness/tier5-src/himi-branded-lite-agenda/index.ts +7 -2
- package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +178 -12
- package/templates/nonprofit-cause/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/nonprofit-cause/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +178 -12
- package/templates/podcast-creator/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/podcast-creator/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +178 -12
- package/templates/portfolio-creative/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/portfolio-creative/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +178 -12
- package/templates/restaurant/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/restaurant/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +178 -12
- package/templates/retail-store/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/retail-store/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +178 -12
- package/templates/salon-spa/README.md +53 -0
- package/templates/salon-spa/config.ts +272 -0
- package/templates/salon-spa/dev/index.ts +19893 -0
- package/templates/salon-spa/himalaya.content.json +49 -0
- package/templates/salon-spa/himalaya.template.json +14 -0
- package/templates/salon-spa/icon.png +0 -0
- package/templates/salon-spa/platforms.json +1 -0
- package/templates/salon-spa/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/salon-spa/tier5-src/_shared/booking-location.ts +147 -0
- package/templates/salon-spa/tier5-src/_shared/booking-session.ts +222 -0
- package/templates/salon-spa/tier5-src/_shared/branded-lite-hosted.ts +278 -0
- package/templates/salon-spa/tier5-src/_shared/branded-load-state.ts +109 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-copy.ts +40 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-history.ts +48 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-message-model.ts +177 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-model.ts +56 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-presenter.ts +581 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-transport.ts +148 -0
- package/templates/salon-spa/tier5-src/_shared/collection-contract.ts +230 -0
- package/templates/salon-spa/tier5-src/_shared/collection-query.ts +193 -0
- package/templates/salon-spa/tier5-src/_shared/collection-service.ts +400 -0
- package/templates/salon-spa/tier5-src/_shared/detail-presence.ts +51 -0
- package/templates/salon-spa/tier5-src/_shared/empty-flags.ts +33 -0
- package/templates/salon-spa/tier5-src/_shared/engine-intl.ts +508 -0
- package/templates/salon-spa/tier5-src/_shared/himi-account.ts +164 -0
- package/templates/salon-spa/tier5-src/_shared/himi-booking-saved.ts +88 -0
- package/templates/salon-spa/tier5-src/_shared/himi-bookings.ts +318 -0
- package/templates/salon-spa/tier5-src/_shared/himi-copy.ts +89 -0
- package/templates/salon-spa/tier5-src/_shared/himi-store.ts +7 -0
- package/templates/salon-spa/tier5-src/_shared/himi-text.ts +24 -0
- package/templates/salon-spa/tier5-src/_shared/host-events.ts +66 -0
- package/templates/salon-spa/tier5-src/_shared/hosted-checkout.ts +70 -0
- package/templates/salon-spa/tier5-src/_shared/iap-attendance-form.ts +106 -0
- package/templates/salon-spa/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
- package/templates/salon-spa/tier5-src/_shared/iap-attendance.ts +38 -0
- package/templates/salon-spa/tier5-src/_shared/iap-worker.ts +125 -0
- package/templates/salon-spa/tier5-src/_shared/local-preferences.ts +38 -0
- package/templates/salon-spa/tier5-src/_shared/member-access.ts +52 -0
- package/templates/salon-spa/tier5-src/_shared/member-account-routes.ts +52 -0
- package/templates/salon-spa/tier5-src/_shared/member-bookings.ts +94 -0
- package/templates/salon-spa/tier5-src/_shared/offset-page.ts +10 -0
- package/templates/salon-spa/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/salon-spa/tier5-src/_shared/owner-load-error.ts +348 -0
- package/templates/salon-spa/tier5-src/_shared/payment-setup.ts +6 -0
- package/templates/salon-spa/tier5-src/_shared/rich-body.ts +78 -0
- package/templates/salon-spa/tier5-src/_shared/rich-content-link.ts +34 -0
- package/templates/salon-spa/tier5-src/_shared/weekly-goal.ts +107 -0
- package/templates/salon-spa/tier5-src/_shared/wix-service-price.ts +146 -0
- package/templates/salon-spa/tier5-src/business-chat/index.ts +669 -0
- package/templates/salon-spa/tier5-src/himi-booking-account/index.ts +241 -0
- package/templates/salon-spa/tier5-src/himi-booking-bookings/index.ts +466 -0
- package/templates/salon-spa/tier5-src/himi-booking-saved/index.ts +103 -0
- package/templates/salon-spa/tier5-src/himi-booking-service/index.ts +1170 -0
- package/templates/salon-spa/tier5-src/himi-booking-services/index.ts +791 -0
- package/templates/salon-spa/tier5-src/himi-booking-time/index.ts +1712 -0
- package/templates/salon-spa/tier5-src/himi-branded-lite-agenda/index.ts +950 -0
- package/templates/salon-spa/tier5-src/himi-branded-lite-galleries/index.ts +282 -0
- package/templates/salon-spa/tier5-src/himi-salon-home/index.ts +544 -0
- package/templates/salon-spa/tier5-src/himi-store-account/index.ts +925 -0
- package/templates/salon-spa/tokens.json +341 -0
- package/templates/yoga-studio/dev/index.ts +192 -0
- package/templates/yoga-studio/tier5-src/_shared/branded-load-state.ts +5 -2
- package/templates/yoga-studio/tier5-src/_shared/himi-account.ts +31 -0
- package/templates/yoga-studio/tier5-src/_shared/himi-booking-saved.ts +88 -0
- package/templates/yoga-studio/tier5-src/himi-booking-service/index.ts +50 -3
- package/templates/yoga-studio/tier5-src/himi-booking-services/index.ts +70 -3
- package/templates/yoga-studio/tier5-src/himi-booking-time/index.ts +40 -8
- package/templates/yoga-studio/tier5-src/himi-branded-lite-agenda/index.ts +7 -2
- package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +178 -12
|
@@ -0,0 +1,278 @@
|
|
|
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", "himi_agenda"], 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_business_chat"], 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_address: ["restaurants", "store"],
|
|
69
|
+
himi_addresses: ["restaurants", "store"],
|
|
70
|
+
himi_agenda: ["bookings"],
|
|
71
|
+
himi_badge: ["badges"],
|
|
72
|
+
himi_badges: ["badges"],
|
|
73
|
+
himi_blog: ["blog"],
|
|
74
|
+
himi_bookings: ["bookings"],
|
|
75
|
+
himi_cart: ["store"],
|
|
76
|
+
himi_business_chat: ["inbox"],
|
|
77
|
+
himi_chat_thread: ["inbox", "members"],
|
|
78
|
+
himi_cms: ["cms"],
|
|
79
|
+
himi_collection: ["store"],
|
|
80
|
+
himi_collections: ["store"],
|
|
81
|
+
himi_coupon: ["coupons"],
|
|
82
|
+
himi_coupons: ["coupons"],
|
|
83
|
+
himi_create_post: ["blog", "forum"],
|
|
84
|
+
himi_donation: ["donations"],
|
|
85
|
+
himi_donations: ["donations"],
|
|
86
|
+
himi_events: ["events"],
|
|
87
|
+
himi_faq: ["faq"],
|
|
88
|
+
himi_feed: ["feed"],
|
|
89
|
+
himi_food_order: ["restaurants"],
|
|
90
|
+
himi_food_orders: ["restaurants"],
|
|
91
|
+
himi_form: ["forms"],
|
|
92
|
+
himi_forms: ["forms"],
|
|
93
|
+
himi_forum: ["forum"],
|
|
94
|
+
himi_forum_post: ["forum"],
|
|
95
|
+
himi_galleries: ["galleries"],
|
|
96
|
+
himi_gallery: ["galleries"],
|
|
97
|
+
himi_gift_card: ["giftCards"],
|
|
98
|
+
himi_gift_cards: ["giftCards"],
|
|
99
|
+
himi_groups: ["groups"],
|
|
100
|
+
himi_group_about: ["groups"],
|
|
101
|
+
himi_my_groups: ["groups"],
|
|
102
|
+
himi_invoice: ["invoices"],
|
|
103
|
+
himi_invoices: ["invoices"],
|
|
104
|
+
himi_loyalty: ["loyalty"],
|
|
105
|
+
himi_loyalty_reward: ["loyalty"],
|
|
106
|
+
himi_member: ["members"],
|
|
107
|
+
himi_members: ["members"],
|
|
108
|
+
himi_menu: ["restaurants"],
|
|
109
|
+
himi_my_events: ["events"],
|
|
110
|
+
himi_my_subscriptions: ["plans"],
|
|
111
|
+
himi_new_message: ["inbox", "members"],
|
|
112
|
+
himi_notifications: ["notifications"],
|
|
113
|
+
himi_orders: ["store"],
|
|
114
|
+
himi_photo_album: ["galleries"],
|
|
115
|
+
himi_place_summary: ["bookings", "plans"],
|
|
116
|
+
himi_plan: ["plans"],
|
|
117
|
+
himi_plans: ["plans"],
|
|
118
|
+
himi_podcast: ["podcasts"],
|
|
119
|
+
himi_podcasts: ["podcasts"],
|
|
120
|
+
himi_portfolio: ["portfolio"],
|
|
121
|
+
himi_portfolio_project: ["portfolio"],
|
|
122
|
+
himi_programs: ["programs"],
|
|
123
|
+
himi_member_programs: ["programs"],
|
|
124
|
+
himi_reservations: ["reservations"],
|
|
125
|
+
himi_reserve: ["restaurants"],
|
|
126
|
+
himi_review: ["reviews"],
|
|
127
|
+
himi_reviews: ["reviews"],
|
|
128
|
+
himi_saved: ["blog"],
|
|
129
|
+
himi_saved_products: ["store"],
|
|
130
|
+
himi_services: ["bookings"],
|
|
131
|
+
himi_shop: ["store"],
|
|
132
|
+
himi_subscription: ["plans"],
|
|
133
|
+
himi_video: ["videos"],
|
|
134
|
+
himi_video_channel: ["videos"],
|
|
135
|
+
himi_videos: ["videos"],
|
|
136
|
+
himi_wallet: ["bookings", "plans", "restaurants", "store"],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** The templated (`<prefix>[_/]<id>`) detail routes each vertical owns, pinned the same way. */
|
|
140
|
+
export const HOSTED_ROUTE_PREFIXES: Readonly<Record<string, readonly BrandedLiteVertical[]>> = {
|
|
141
|
+
himi_order: ["store"],
|
|
142
|
+
himi_product: ["store"],
|
|
143
|
+
himi_service: ["bookings"],
|
|
144
|
+
himi_time: ["bookings"],
|
|
145
|
+
himi_menu_item: ["restaurants"],
|
|
146
|
+
himi_program: ["programs"],
|
|
147
|
+
himi_event: ["events"],
|
|
148
|
+
himi_rsvp: ["events"],
|
|
149
|
+
himi_blog_post: ["blog"],
|
|
150
|
+
himi_group: ["groups"],
|
|
151
|
+
himi_cms_collection: ["cms"],
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** The verticals that own [screenId], or undefined for a screen every site has (Home, Profile…). */
|
|
155
|
+
export function screenVerticals(screenId: string): readonly BrandedLiteVertical[] | undefined {
|
|
156
|
+
const exact = HOSTED_SCREEN_VERTICALS[screenId];
|
|
157
|
+
if (exact) return exact;
|
|
158
|
+
// The registry's `^<prefix>(?:[_/](.+))?$`: the bare prefix, or the prefix, a separator and an id.
|
|
159
|
+
for (const [prefix, verticals] of Object.entries(HOSTED_ROUTE_PREFIXES)) {
|
|
160
|
+
if (screenId === prefix) return verticals;
|
|
161
|
+
const sep = screenId.charAt(prefix.length);
|
|
162
|
+
if (screenId.startsWith(prefix) && (sep === "_" || sep === "/") && screenId.length > prefix.length + 1) return verticals;
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The drawer's collapsible folders and the verticals in each (config.ts builds them from this).
|
|
168
|
+
* A folder header is hidden when none of its verticals shows, so a site never sees an empty one. */
|
|
169
|
+
export const HOSTED_DRAWER_FOLDERS: Readonly<Record<string, readonly BrandedLiteVertical[]>> = {
|
|
170
|
+
drawer_sec_community: ["feed", "forum", "groups", "members", "reviews", "badges", "blog"],
|
|
171
|
+
drawer_sec_media: ["videos", "podcasts", "portfolio", "cms", "galleries", "faq"],
|
|
172
|
+
drawer_sec_offers: ["coupons", "giftCards", "donations", "loyalty"],
|
|
173
|
+
drawer_sec_activity: ["inbox", "reservations", "invoices", "forms", "notifications", "notifsettings"],
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/** The vertical a nav row's screen belongs to, or undefined (Home, Profile, Steps…). */
|
|
177
|
+
export function verticalOfScreen(screenId: string | undefined): BrandedLiteVertical | undefined {
|
|
178
|
+
if (!screenId) return undefined;
|
|
179
|
+
for (const [vertical, roots] of Object.entries(VERTICAL_ROOT_SCREENS) as [string, readonly string[]][]) {
|
|
180
|
+
if (roots.includes(screenId)) return vertical;
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The screen a route to an uninstalled vertical lands on. */
|
|
186
|
+
export const NOT_AVAILABLE_SCREEN = "himi_not_available";
|
|
187
|
+
|
|
188
|
+
const HEX = /^#[0-9A-Fa-f]{6}$/;
|
|
189
|
+
const VERTICALS = new Set(Object.keys(VERTICAL_ROOT_SCREENS));
|
|
190
|
+
|
|
191
|
+
/** The host's site, or null when it is missing or malformed — a hosted build then shows no
|
|
192
|
+
* vertical at all (fail closed), never every one. */
|
|
193
|
+
export function readHostedSite(raw: unknown): HostedSite | null {
|
|
194
|
+
let value = raw;
|
|
195
|
+
if (typeof value === "string") {
|
|
196
|
+
try { value = JSON.parse(value); } catch { return null; }
|
|
197
|
+
}
|
|
198
|
+
if (!value || typeof value !== "object") return null;
|
|
199
|
+
const v = value as Record<string, unknown>;
|
|
200
|
+
const metaSiteId = typeof v.metaSiteId === "string" ? v.metaSiteId.trim() : "";
|
|
201
|
+
if (!metaSiteId || !Array.isArray(v.installedVerticals)) return null;
|
|
202
|
+
const installed = v.installedVerticals.filter((x): x is BrandedLiteVertical => typeof x === "string" && VERTICALS.has(x));
|
|
203
|
+
const color = typeof v.primaryColor === "string" && HEX.test(v.primaryColor) ? v.primaryColor : "";
|
|
204
|
+
return {
|
|
205
|
+
metaSiteId,
|
|
206
|
+
name: typeof v.name === "string" ? v.name : "",
|
|
207
|
+
installedVerticals: [...new Set(installed)],
|
|
208
|
+
primaryColor: color,
|
|
209
|
+
logoUrl: typeof v.logoUrl === "string" && /^https:\/\//.test(v.logoUrl) ? v.logoUrl : "",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The verticals a hosted build shows for [site]: its installed ones plus the always-on ones.
|
|
214
|
+
* None at all without a site. */
|
|
215
|
+
export function hostedVerticals(site: HostedSite | null): Set<BrandedLiteVertical> {
|
|
216
|
+
if (!site) return new Set();
|
|
217
|
+
return new Set<BrandedLiteVertical>([...site.installedVerticals, ...HOSTED_ALWAYS]);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** `hiddenPath` flags (true hides the row), for the nav worker to publish: one per vertical, and
|
|
221
|
+
* one per drawer folder (hidden when none of its verticals shows). */
|
|
222
|
+
export function hostedHiddenFlags(site: HostedSite | null): Record<string, boolean> {
|
|
223
|
+
const shown = hostedVerticals(site);
|
|
224
|
+
const out: Record<string, boolean> = {};
|
|
225
|
+
for (const v of Object.keys(VERTICAL_ROOT_SCREENS) as BrandedLiteVertical[]) out[v] = !shown.has(v);
|
|
226
|
+
for (const [folder, verticals] of Object.entries(HOSTED_DRAWER_FOLDERS)) out[folder] = !verticals.some((v) => shown.has(v));
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Where a route to [screenId] lands for [site]: itself, or the "not available on this site" screen
|
|
231
|
+
* when every vertical that owns it is missing from the site. A screen no vertical owns (Home,
|
|
232
|
+
* Profile, settings) is on every site and passes through. */
|
|
233
|
+
export function hostedRoute(screenId: string, site: HostedSite | null): string {
|
|
234
|
+
const owners = screenVerticals(screenId);
|
|
235
|
+
if (!owners) return screenId;
|
|
236
|
+
const shown = hostedVerticals(site);
|
|
237
|
+
return owners.some((v) => shown.has(v)) ? screenId : NOT_AVAILABLE_SCREEN;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** One hosted Home "Explore" card. The descriptor hands the worker the whole catalog (its copy is
|
|
241
|
+
* build-time localized) as `himalaya.input.hostedExplore`; the worker publishes only the site's. */
|
|
242
|
+
export interface HostedExploreCard {
|
|
243
|
+
vertical: BrandedLiteVertical;
|
|
244
|
+
icon: string;
|
|
245
|
+
title: string;
|
|
246
|
+
body: string;
|
|
247
|
+
/** Always the vertical's own root screen — never taken from the input. */
|
|
248
|
+
screenId: string;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** The Explore cards to show for [site], in catalog order: one per catalog entry whose vertical the
|
|
252
|
+
* site shows. Empty without a site or a catalog, so a worker that never boots shows no card at all
|
|
253
|
+
* (the grid binds this list — fail closed, unlike a per-card `hiddenPath`, which native renderers
|
|
254
|
+
* treat as visible until it resolves). */
|
|
255
|
+
export function hostedExploreCards(catalog: unknown, site: HostedSite | null): HostedExploreCard[] {
|
|
256
|
+
if (!Array.isArray(catalog)) return [];
|
|
257
|
+
const shown = hostedVerticals(site);
|
|
258
|
+
const out: HostedExploreCard[] = [];
|
|
259
|
+
const seen = new Set<string>();
|
|
260
|
+
for (const entry of catalog) {
|
|
261
|
+
if (!entry || typeof entry !== "object") continue;
|
|
262
|
+
const e = entry as Record<string, unknown>;
|
|
263
|
+
const vertical = typeof e.vertical === "string" ? e.vertical : "";
|
|
264
|
+
const screenId = VERTICAL_ROOT_SCREENS[vertical]?.[0];
|
|
265
|
+
if (!screenId || !shown.has(vertical) || seen.has(vertical)) continue;
|
|
266
|
+
seen.add(vertical);
|
|
267
|
+
out.push({
|
|
268
|
+
vertical, screenId,
|
|
269
|
+
icon: typeof e.icon === "string" ? e.icon : "grid",
|
|
270
|
+
title: typeof e.title === "string" ? e.title : "",
|
|
271
|
+
body: typeof e.body === "string" ? e.body : "",
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** The `$service` path a hosted nav row's visibility binds to (a vertical, or a drawer folder id). */
|
|
278
|
+
export const hostedHiddenPath = (key: string): string => `$service:brandedLiteNav.hosted.${key}`;
|
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
/** hiddenWhen gate for the empty state: false ONLY when the site answered with no rows
|
|
39
|
+
* (`notMockEmpty` is true then — it gates the sample banner, not the empty state). */
|
|
40
|
+
hideEmpty: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const LOADING: LoadFlags = {
|
|
44
|
+
status: "loading", error: "", signInRequired: false, notErrored: true, notLoading: false, busy: true, noSignIn: true,
|
|
45
|
+
isMock: false, notMock: true, notMockEmpty: true, notMockUnavailable: true, hideEmpty: true,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const READY: LoadFlags = { ...LOADING, status: "ready", notLoading: true, busy: false };
|
|
49
|
+
|
|
50
|
+
/** Did the HOST ask for sample data? Only the web preview's sample mode does; a native
|
|
51
|
+
* build, a visitor preview and a live owner preview never carry it. */
|
|
52
|
+
export function samplesRequested(): boolean {
|
|
53
|
+
const input = (globalThis as { himalaya?: { input?: Record<string, unknown> } }).himalaya?.input;
|
|
54
|
+
return input?.__himiOwnerPreview === "sample";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Wix refused an ANONYMOUS visitor: signing in is the fix, not retrying. A 403 is not
|
|
58
|
+
* this — it also answers "feature not installed" and a signed-in member without the
|
|
59
|
+
* permission, where a Sign in button leads nowhere — so it stays a Retry-able failure. */
|
|
60
|
+
export function needsSignIn(err: unknown): boolean {
|
|
61
|
+
const msg = String((err as { message?: unknown })?.message ?? err ?? "");
|
|
62
|
+
return /\b401\b|unauthori[sz]ed|unauthenticated/i.test(msg);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface LoadCopy<T> {
|
|
66
|
+
/** The copy namespace (`giftCards`, `coupons`, …) — keys resolve through copyParam. */
|
|
67
|
+
prefix: string;
|
|
68
|
+
/** Shown when the site genuinely has none. */
|
|
69
|
+
empty: string;
|
|
70
|
+
/** Shown when the query failed. */
|
|
71
|
+
error: string;
|
|
72
|
+
/** Shown when Wix refused the visitor (member-only data). */
|
|
73
|
+
signIn: string;
|
|
74
|
+
/** Sample rows for the explicit sample preview ONLY. */
|
|
75
|
+
sample: () => T[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type Rows<K extends string, T> = { [P in K]: T[] } & { emptyText: string } & LoadFlags;
|
|
79
|
+
|
|
80
|
+
/** A query answered. Rows win; an empty answer is a real empty state. */
|
|
81
|
+
export function loaded<K extends string, T>(key: K, rows: T[], copy: LoadCopy<T>): Rows<K, T> {
|
|
82
|
+
if (rows.length) return { ...READY, [key]: rows, emptyText: "" } as Rows<K, T>;
|
|
83
|
+
if (samplesRequested()) return sampleRows(key, copy, "empty");
|
|
84
|
+
return { ...READY, [key]: [], emptyText: copyParam(`${copy.prefix}.emptyText`, copy.empty), hideEmpty: false } as Rows<K, T>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A query threw. Never rows the customer did not create. */
|
|
88
|
+
export function loadFailed<K extends string, T>(key: K, err: unknown, copy: LoadCopy<T>): Rows<K, T> {
|
|
89
|
+
if (samplesRequested()) return sampleRows(key, copy, "unavailable");
|
|
90
|
+
const signIn = needsSignIn(err);
|
|
91
|
+
return {
|
|
92
|
+
...READY,
|
|
93
|
+
status: "error",
|
|
94
|
+
[key]: [],
|
|
95
|
+
emptyText: "",
|
|
96
|
+
error: signIn ? copyParam(`${copy.prefix}.signInText`, copy.signIn) : copyParam(`${copy.prefix}.errorText`, copy.error),
|
|
97
|
+
signInRequired: signIn,
|
|
98
|
+
notErrored: false,
|
|
99
|
+
busy: true,
|
|
100
|
+
noSignIn: !signIn,
|
|
101
|
+
} as Rows<K, T>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function sampleRows<K extends string, T>(key: K, copy: LoadCopy<T>, reason: "empty" | "unavailable"): Rows<K, T> {
|
|
105
|
+
return {
|
|
106
|
+
...READY, [key]: copy.sample(), emptyText: "",
|
|
107
|
+
isMock: true, notMock: false, notMockEmpty: reason !== "empty", notMockUnavailable: reason !== "unavailable",
|
|
108
|
+
} as Rows<K, T>;
|
|
109
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Shared defaults keep descriptor copy and worker fallbacks aligned.
|
|
2
|
+
import { copyParam } from "./himi-copy.js";
|
|
3
|
+
|
|
4
|
+
export const BUSINESS_CHAT_COPY = {
|
|
5
|
+
"emptyTitle": "No messages yet",
|
|
6
|
+
"sending": "Sending…",
|
|
7
|
+
"unconfirmed": "Not confirmed",
|
|
8
|
+
"sessionChanged": "Your member session changed. Sign in again to continue.",
|
|
9
|
+
"hostUnavailable": "This app host cannot reach the inbox yet.",
|
|
10
|
+
"sendFailed": "Message delivery could not be confirmed. Check the conversation before trying again.",
|
|
11
|
+
"openFailed": "Couldn’t open the conversation. Try again.",
|
|
12
|
+
"loadFailed": "Couldn’t load the conversation. Try again.",
|
|
13
|
+
"signInPrompt": "Sign in to send a message.",
|
|
14
|
+
"rejected": "The message was rejected. Your draft is retained.",
|
|
15
|
+
"checkDelivery": "No automatic retry was made. Reopen the conversation to check whether it arrived.",
|
|
16
|
+
"suggestionSent": "Suggestion sent.",
|
|
17
|
+
"suggestionRejected": "The suggestion was rejected.",
|
|
18
|
+
"suggestionUnconfirmed": "Suggestion delivery is unconfirmed; it will not be sent again automatically.",
|
|
19
|
+
"productImage": "Product image",
|
|
20
|
+
"connecting": "Connecting to live updates",
|
|
21
|
+
"reconnecting": "Reconnecting live updates",
|
|
22
|
+
"liveSessionUnavailable": "Live updates unavailable for this session",
|
|
23
|
+
"liveHostUnavailable": "Live updates unavailable in this host",
|
|
24
|
+
"liveSetupFailed": "Live connection setup failed",
|
|
25
|
+
"liveAuthFailed": "Live connection authentication failed",
|
|
26
|
+
"liveInterrupted": "Live connection interrupted",
|
|
27
|
+
"liveUnreadable": "Live update could not be read",
|
|
28
|
+
"livePaused": "Live updates paused",
|
|
29
|
+
"liveUnavailable": "Live updates unavailable",
|
|
30
|
+
"openLink": "Open link",
|
|
31
|
+
"today": "Today",
|
|
32
|
+
"yesterday": "Yesterday",
|
|
33
|
+
"aiAssistant": "AI assistant",
|
|
34
|
+
"staffResponse": "Staff Response",
|
|
35
|
+
"imageAttachment": "Image attachment"
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
export function businessChatCopy(key: keyof typeof BUSINESS_CHAT_COPY): string {
|
|
39
|
+
return copyParam(`businessChat.${key}`, BUSINESS_CHAT_COPY[key]);
|
|
40
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Smart Chat resolves its conversation from the caller's Wix identity. Its history must not
|
|
2
|
+
* be replaced by a different conversation returned by the Members chat room list. */
|
|
3
|
+
type Raw = Record<string, unknown>;
|
|
4
|
+
const object = (value: unknown): Raw => value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Raw : {};
|
|
5
|
+
const text = (value: unknown): string => typeof value === 'string' ? value : '';
|
|
6
|
+
|
|
7
|
+
// Positive lists: navigation, cart events, owner notifications and training messages are not a transcript.
|
|
8
|
+
const VISITOR_TYPES = new Set(['QUESTION', 'VISITOR_MESSAGE', 'VISITOR_OPTION_SELECTED', 'FAQ_QUESTION']);
|
|
9
|
+
const ASSISTANT_TYPES = new Set(['ANSWER', 'INTRO', 'LEGAL', 'OFFLINE', 'OUT_OF_QUOTA', 'FAQ_ANSWER']);
|
|
10
|
+
const STAFF_TYPES = new Set(['USER_ANSWER', 'WIX_USER_CARD', 'WIX_USER_MINIMAL_BODY', 'WIX_USER_MEDIA', 'WIX_USER_FORM']);
|
|
11
|
+
|
|
12
|
+
/** Adapt the current identity-scoped Smart Chat conversation to the existing presentation pipeline.
|
|
13
|
+
* `inboxId` identifies an Inbox MESSAGE, never a room. Where that exact message exists in the
|
|
14
|
+
* member page, keep its rich content; otherwise use the persisted Smart Chat body. No text/id guesses. */
|
|
15
|
+
export function smartChatHistory(aiMessages: unknown[], memberPage: unknown[], participantId: string): { conversationId: string; messages: Raw[] } | null {
|
|
16
|
+
const dated = aiMessages.map(object)
|
|
17
|
+
.filter(message => text(message.id) && text(message.conversationId) && Number.isFinite(Date.parse(text(message.createdDate))))
|
|
18
|
+
.sort((a, b) => Date.parse(text(a.createdDate)) - Date.parse(text(b.createdDate)));
|
|
19
|
+
const conversationId = text(dated.at(-1)?.conversationId);
|
|
20
|
+
if (!conversationId) return null;
|
|
21
|
+
const memberRows = new Map(memberPage.map(object).map(row => [text(row.id), row]));
|
|
22
|
+
const messages: Raw[] = [];
|
|
23
|
+
for (const message of dated) {
|
|
24
|
+
if (message.conversationId !== conversationId) continue;
|
|
25
|
+
const sender = text(message.sender);
|
|
26
|
+
const kind = text(message.messageType);
|
|
27
|
+
const mine = sender === 'SITE_VISITOR' && VISITOR_TYPES.has(kind);
|
|
28
|
+
if (!mine && !(sender === 'ASSISTANT' && ASSISTANT_TYPES.has(kind)) && !(sender === 'USER' && STAFF_TYPES.has(kind))) continue;
|
|
29
|
+
const body = object(message.body);
|
|
30
|
+
const bodyText = [text(body.mainText), text(body.footerText)].filter(Boolean).join('\n');
|
|
31
|
+
const rich = memberRows.get(text(message.inboxId));
|
|
32
|
+
// A quick-reply-only metadata answer is not an empty bubble (the options are shown by the composer).
|
|
33
|
+
if (!bodyText && !rich) continue;
|
|
34
|
+
messages.push({
|
|
35
|
+
...rich,
|
|
36
|
+
id: `smart-chat:${message.id}`,
|
|
37
|
+
// Member-room sequence numbers cannot order a different Smart Chat conversation.
|
|
38
|
+
sequence: undefined,
|
|
39
|
+
createdDate: message.createdDate,
|
|
40
|
+
sender: { userId: mine ? participantId : `smart-chat:${sender}` },
|
|
41
|
+
aiSiteChatSender: sender,
|
|
42
|
+
messageType: rich?.messageType ?? 'message',
|
|
43
|
+
data: rich && (Array.isArray(rich.data) && rich.data.length || rich.templateData)
|
|
44
|
+
? rich.data : [{ mimeType: 'text/plain', data: { text: bodyText } }],
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return messages.length ? { conversationId, messages } : null;
|
|
48
|
+
}
|