@wix/himalaya-cli 0.1588.0 → 0.1590.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/sdk-types/src/capabilities.generated.d.ts +2 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/torch/src/index.d.ts +39 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/webview-page/src/capabilities.generated.d.ts +2 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/define-app.d.ts +13 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/groups/src/extensions.d.ts +28 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/members-v1/src/index.d.ts +9 -5
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/audio-record.mjs +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calendar.mjs +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/calls.mjs +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/contacts.mjs +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/location.mjs +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/motion.mjs +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/photos.mjs +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdk-types.mjs +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/tasks.mjs +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/torch.mjs +78 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-groups.mjs +14 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-members.mjs +9 -8
- package/dist/authoring-runtime/node_modules/@wix/himalaya/package.json +4 -0
- package/dist/cli.mjs +121 -50
- package/dist/preview-assets.generated.bin +0 -0
- package/fixtures/wix-canned.json +16 -0
- package/guides/app-config.md +1 -1
- package/guides/importable-sdks.md +1 -0
- package/guides/one-app-to-himi-app.md +18 -4
- package/guides/qa-builds.md +12 -0
- package/guides/sdk-reference.json +101 -2
- package/guides/web-page-sdk-reference.md +1 -0
- package/package.json +2 -2
- package/templates/beauty-salon/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/beauty-salon/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/beauty-salon/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/beauty-salon/tier5-src/_shared/group-member-count.ts +15 -0
- package/templates/beauty-salon/tier5-src/_shared/himi-bookings.ts +54 -13
- package/templates/beauty-salon/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/beauty-salon/tier5-src/_shared/himi-restaurants.ts +2 -2
- package/templates/beauty-salon/tier5-src/_shared/restaurant-ordering.ts +1 -1
- package/templates/beauty-salon/tier5-src/_shared/wix-service-price.ts +2 -2
- package/templates/beauty-salon/tier5-src/himi-booking-services/index.ts +8 -3
- package/templates/beauty-salon/tier5-src/himi-branded-lite-cart/index.ts +1 -1
- package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +2 -1
- package/templates/beauty-salon/tier5-src/himi-branded-lite-plans/index.ts +1 -1
- package/templates/beauty-salon-owner/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/beauty-salon-owner/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/branded-lite/config.ts +7 -0
- package/templates/branded-lite/dev/index.ts +469 -0
- package/templates/branded-lite/himalaya.content.json +3 -0
- package/templates/branded-lite/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/branded-lite/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/branded-lite/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/branded-lite/tier5-src/_shared/group-member-count.ts +15 -0
- package/templates/branded-lite/tier5-src/_shared/himi-bookings.ts +54 -13
- package/templates/branded-lite/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/branded-lite/tier5-src/_shared/himi-restaurants.ts +2 -2
- package/templates/branded-lite/tier5-src/_shared/restaurant-ordering.ts +1 -1
- package/templates/branded-lite/tier5-src/_shared/wix-service-price.ts +2 -2
- package/templates/branded-lite/tier5-src/himi-booking-services/index.ts +8 -3
- package/templates/branded-lite/tier5-src/himi-branded-lite-cart/index.ts +1 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-group/index.ts +9 -3
- package/templates/branded-lite/tier5-src/himi-branded-lite-group-about/index.ts +2 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-groups/index.ts +6 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +2 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-loyalty/index.ts +3 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-my-groups/index.ts +237 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-plans/index.ts +1 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +1 -1
- package/templates/branded-lite/tier5-src/himi-events-detail/index.ts +5 -5
- package/templates/branded-lite/tier5-src/wix-spaces-member-detail/index.ts +5 -1
- package/templates/brandy/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/brandy/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/community-club/config.ts +7 -0
- package/templates/community-club/dev/index.ts +469 -0
- package/templates/community-club/himalaya.content.json +3 -0
- package/templates/community-club/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/community-club/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/community-club/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/community-club/tier5-src/_shared/group-member-count.ts +15 -0
- package/templates/community-club/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/community-club/tier5-src/_shared/himi-restaurants.ts +2 -2
- package/templates/community-club/tier5-src/_shared/wix-service-price.ts +2 -2
- package/templates/community-club/tier5-src/himi-branded-lite-group/index.ts +9 -3
- package/templates/community-club/tier5-src/himi-branded-lite-group-about/index.ts +2 -1
- package/templates/community-club/tier5-src/himi-branded-lite-groups/index.ts +6 -2
- package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +2 -1
- package/templates/community-club/tier5-src/himi-branded-lite-my-groups/index.ts +237 -0
- package/templates/community-club/tier5-src/himi-events-detail/index.ts +5 -5
- package/templates/community-club-owner/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/community-club-owner/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/community-club-owner/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/events-meetups/config.ts +7 -0
- package/templates/events-meetups/dev/index.ts +469 -0
- package/templates/events-meetups/himalaya.content.json +3 -0
- package/templates/events-meetups/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/events-meetups/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/events-meetups/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/events-meetups/tier5-src/_shared/group-member-count.ts +15 -0
- package/templates/events-meetups/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/events-meetups/tier5-src/_shared/himi-restaurants.ts +2 -2
- package/templates/events-meetups/tier5-src/_shared/wix-service-price.ts +2 -2
- package/templates/events-meetups/tier5-src/himi-branded-lite-group/index.ts +9 -3
- package/templates/events-meetups/tier5-src/himi-branded-lite-group-about/index.ts +2 -1
- package/templates/events-meetups/tier5-src/himi-branded-lite-groups/index.ts +6 -2
- package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +2 -1
- package/templates/events-meetups/tier5-src/himi-branded-lite-my-groups/index.ts +237 -0
- package/templates/events-meetups/tier5-src/himi-events-detail/index.ts +5 -5
- package/templates/events-meetups-owner/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/events-meetups-owner/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/events-meetups-owner/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/gym-fitness/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/gym-fitness/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/gym-fitness/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/gym-fitness/tier5-src/_shared/group-member-count.ts +15 -0
- package/templates/gym-fitness/tier5-src/_shared/himi-bookings.ts +54 -13
- package/templates/gym-fitness/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/gym-fitness/tier5-src/_shared/himi-restaurants.ts +2 -2
- package/templates/gym-fitness/tier5-src/_shared/restaurant-ordering.ts +1 -1
- package/templates/gym-fitness/tier5-src/_shared/wix-service-price.ts +2 -2
- package/templates/gym-fitness/tier5-src/himi-booking-services/index.ts +8 -3
- package/templates/gym-fitness/tier5-src/himi-branded-lite-cart/index.ts +1 -1
- package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +2 -1
- package/templates/gym-fitness/tier5-src/himi-branded-lite-plans/index.ts +1 -1
- package/templates/gym-fitness/tier5-src/himi-events-detail/index.ts +5 -5
- package/templates/gym-fitness-owner/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/gym-fitness-owner/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/himi-chat/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/himi-chat/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/nonprofit-cause/config.ts +7 -0
- package/templates/nonprofit-cause/dev/index.ts +469 -0
- package/templates/nonprofit-cause/himalaya.content.json +3 -0
- package/templates/nonprofit-cause/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/nonprofit-cause/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/nonprofit-cause/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/nonprofit-cause/tier5-src/_shared/group-member-count.ts +15 -0
- package/templates/nonprofit-cause/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/nonprofit-cause/tier5-src/_shared/himi-restaurants.ts +2 -2
- package/templates/nonprofit-cause/tier5-src/_shared/wix-service-price.ts +2 -2
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-group/index.ts +9 -3
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-group-about/index.ts +2 -1
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-groups/index.ts +6 -2
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +2 -1
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-my-groups/index.ts +237 -0
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-plans/index.ts +1 -1
- package/templates/nonprofit-cause/tier5-src/himi-events-detail/index.ts +5 -5
- package/templates/nonprofit-cause-owner/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/nonprofit-cause-owner/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/owner-lite/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/owner-lite/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/owner-lite/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/podcast-creator/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/podcast-creator/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/podcast-creator/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/podcast-creator/tier5-src/_shared/group-member-count.ts +15 -0
- package/templates/podcast-creator/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/podcast-creator/tier5-src/_shared/himi-restaurants.ts +2 -2
- package/templates/podcast-creator/tier5-src/_shared/wix-service-price.ts +2 -2
- package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +2 -1
- package/templates/podcast-creator/tier5-src/himi-branded-lite-plans/index.ts +1 -1
- package/templates/podcast-creator/tier5-src/himi-events-detail/index.ts +5 -5
- package/templates/podcast-creator-owner/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/podcast-creator-owner/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/portfolio-creative/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/portfolio-creative/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/portfolio-creative/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/portfolio-creative/tier5-src/_shared/group-member-count.ts +15 -0
- package/templates/portfolio-creative/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/portfolio-creative/tier5-src/_shared/himi-restaurants.ts +2 -2
- package/templates/portfolio-creative/tier5-src/_shared/restaurant-ordering.ts +1 -1
- package/templates/portfolio-creative/tier5-src/_shared/wix-service-price.ts +2 -2
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-cart/index.ts +1 -1
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +2 -1
- package/templates/portfolio-creative/tier5-src/himi-events-detail/index.ts +5 -5
- package/templates/portfolio-creative-owner/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/portfolio-creative-owner/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/restaurant/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/restaurant/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/restaurant/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/restaurant/tier5-src/_shared/group-member-count.ts +15 -0
- package/templates/restaurant/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/restaurant/tier5-src/_shared/himi-restaurants.ts +2 -2
- package/templates/restaurant/tier5-src/_shared/restaurant-ordering.ts +1 -1
- package/templates/restaurant/tier5-src/_shared/wix-service-price.ts +2 -2
- package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +2 -1
- package/templates/restaurant/tier5-src/himi-events-detail/index.ts +5 -5
- package/templates/restaurant-owner/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/restaurant-owner/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/restaurant-owner/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/retail-store/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/retail-store/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/retail-store/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/retail-store/tier5-src/_shared/group-member-count.ts +15 -0
- package/templates/retail-store/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/retail-store/tier5-src/_shared/himi-restaurants.ts +2 -2
- package/templates/retail-store/tier5-src/_shared/restaurant-ordering.ts +1 -1
- package/templates/retail-store/tier5-src/_shared/wix-service-price.ts +2 -2
- package/templates/retail-store/tier5-src/himi-branded-lite-cart/index.ts +1 -1
- package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +2 -1
- package/templates/retail-store-owner/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/retail-store-owner/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/retail-store-owner/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/yoga-studio/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/yoga-studio/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/yoga-studio/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/yoga-studio/tier5-src/_shared/group-member-count.ts +15 -0
- package/templates/yoga-studio/tier5-src/_shared/himi-bookings.ts +54 -13
- package/templates/yoga-studio/tier5-src/_shared/himi-copy.ts +10 -2
- package/templates/yoga-studio/tier5-src/_shared/himi-restaurants.ts +2 -2
- package/templates/yoga-studio/tier5-src/_shared/wix-service-price.ts +2 -2
- package/templates/yoga-studio/tier5-src/himi-booking-services/index.ts +8 -3
- package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +2 -1
- package/templates/yoga-studio/tier5-src/himi-branded-lite-plans/index.ts +1 -1
- package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +1 -1
- package/templates/yoga-studio/tier5-src/himi-events-detail/index.ts +5 -5
- package/templates/yoga-studio-owner/tier5-src/_shared/app-locale.ts +61 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/engine-intl.ts +5 -3
- package/templates/yoga-studio-owner/tier5-src/_shared/himi-copy.ts +10 -2
|
Binary file
|
package/fixtures/wix-canned.json
CHANGED
|
@@ -1884,6 +1884,22 @@
|
|
|
1884
1884
|
"comments": [],
|
|
1885
1885
|
"commentCount": 0
|
|
1886
1886
|
},
|
|
1887
|
+
"GET /social-groups-proxy/members/v2/members/mem-0001/memberships": {
|
|
1888
|
+
"memberships": [
|
|
1889
|
+
{
|
|
1890
|
+
"groupId": "grp-0001",
|
|
1891
|
+
"status": "JOINED",
|
|
1892
|
+
"role": {
|
|
1893
|
+
"value": "MEMBER"
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
],
|
|
1897
|
+
"metadata": {
|
|
1898
|
+
"count": 1,
|
|
1899
|
+
"offset": 0,
|
|
1900
|
+
"total": 1
|
|
1901
|
+
}
|
|
1902
|
+
},
|
|
1887
1903
|
"GET /social-groups-proxy/groups/v2/groups/grp-0001": {
|
|
1888
1904
|
"group": {
|
|
1889
1905
|
"id": "grp-0001",
|
package/guides/app-config.md
CHANGED
|
@@ -137,7 +137,7 @@ does not include answers `module_not_linked` on device. Some components need one
|
|
|
137
137
|
and `himi validate` errors on a screen that uses one the app does not declare.
|
|
138
138
|
`presigned_upload.putFile` also requires `files` on the calling screen.
|
|
139
139
|
|
|
140
|
-
App-level capability vocabulary: `camera`, `barcode_scanner`, `biometrics`, `secure_storage`, `share`, `push`, `file_picker`, `files`, `open_url`, `pdf`, `clipboard`, `theme`, `l10n`, `photos`, `presigned_upload`, `ui`, `calendar`, `contacts`, `tasks`, `audio_record`, `audio_record_background`, `calls`, `location`, `location_background`, `motion`, `health`, `media_playback`, `webview`, `browser_auth`, `haptic`, `himi_apps`, `iap`, `keyboard`, `network`, `wix_cashier`, `wix_chat`, `live_activity`, `local_database`, `speech`, `speaker_diarization`, `system_intents`, `on_device_ai`, `app_info`, `release_control`, `godot`, `base44`, `animation`.
|
|
140
|
+
App-level capability vocabulary: `camera`, `barcode_scanner`, `biometrics`, `secure_storage`, `share`, `push`, `file_picker`, `files`, `open_url`, `pdf`, `clipboard`, `theme`, `l10n`, `photos`, `presigned_upload`, `ui`, `calendar`, `contacts`, `tasks`, `audio_record`, `audio_record_background`, `calls`, `location`, `location_background`, `motion`, `health`, `media_playback`, `webview`, `browser_auth`, `haptic`, `himi_apps`, `iap`, `keyboard`, `torch`, `network`, `wix_cashier`, `wix_chat`, `live_activity`, `local_database`, `speech`, `speaker_diarization`, `system_intents`, `on_device_ai`, `app_info`, `release_control`, `godot`, `base44`, `animation`.
|
|
141
141
|
|
|
142
142
|
## What `himi validate` checks (7 passes)
|
|
143
143
|
|
|
@@ -63,6 +63,7 @@ package in one greppable file) or `GET /v1/sdk/<name>` over HTTP; in the CLI,
|
|
|
63
63
|
- `@himalaya/tasks` — Tier-5 typed binding for reminders / to-dos.
|
|
64
64
|
- `@himalaya/theme` — Tier-5 typed binding for the runtime brand overlay.
|
|
65
65
|
- `@himalaya/time` — Tier-5 temporal helpers.
|
|
66
|
+
- `@himalaya/torch` — Tier-5 typed binding for the platform's torch (the camera flashlight).
|
|
66
67
|
- `@himalaya/ui` — Tier-5 typed binding to the platform's imperative popup capability.
|
|
67
68
|
- `@himalaya/webview` — Tier-5 typed binding for the page ↔ worker message channel of a worker-backed webview screen (Part 2 / RFC 0025 composition).
|
|
68
69
|
- `@himalaya/webview-page` — browser-side SDK for a trusted page hosted by a worker-backed Himalaya WebView.
|
|
@@ -232,10 +232,14 @@ private/all-members total.
|
|
|
232
232
|
Member-profile follower/following totals are separate runtime reads, not the
|
|
233
233
|
directory total or this collected snapshot. The shared member-detail worker keeps
|
|
234
234
|
the public profile readable when its activity counters are forbidden, missing or
|
|
235
|
-
invalid, and hides the count line.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
235
|
+
invalid, and hides the count line. Wix keeps one counter per direction and creates
|
|
236
|
+
it on the first change, so a member can have only `followerCount` or only
|
|
237
|
+
`followingCount`: `getFollowCounts` reports each direction on its own, as an
|
|
238
|
+
explicit nonnegative safe integer from the requested member's Followers-app entry
|
|
239
|
+
or `null` when Wix returned none, reading the source's keyed counter array (and
|
|
240
|
+
compatible older map responses). A missing counter is hidden and never becomes
|
|
241
|
+
zero; a real zero remains visible; a malformed counter, an entry with neither
|
|
242
|
+
counter or a permission failure rejects and hides the line.
|
|
239
243
|
Following a member does not turn an unknown aggregate into a fabricated total.
|
|
240
244
|
Late Follow results cannot restore counters from an earlier load, identity or site.
|
|
241
245
|
Directory search metadata, activity and viewed-member program progress require
|
|
@@ -394,6 +398,16 @@ Apply these interpretation rules:
|
|
|
394
398
|
conversion details; locale, device settings and permission failures are not currency
|
|
395
399
|
evidence. Require recorded live Visitor catalog and signed-in own-account responses
|
|
396
400
|
for acceptance; mocked member balances prove implementation behavior only.
|
|
401
|
+
- **Bookings: bake the business time zone.** A course's session times (`firstSessionStart`,
|
|
402
|
+
`lastSessionEnd`) may arrive without an offset, and only the business's own zone places them
|
|
403
|
+
(the "Ended" label). A `bookings_services_list_section` collection reads the same public,
|
|
404
|
+
site-bound Visitor properties and allowlists `businessContext.timeZone`, kept only when it is an
|
|
405
|
+
IANA zone ("Asia/Jerusalem"). When verified, bake it into Branded provisioning
|
|
406
|
+
`businessTimeZone` (the source value, never an example, a locale guess or the device zone). The
|
|
407
|
+
worker cannot read it itself: `site-properties/v4` is owner-scoped. Omit an unverified zone: the
|
|
408
|
+
list then shows no Ended label for a time without an offset, which is correct, and a
|
|
409
|
+
`business_time_zone_unavailable` warning says so. A re-apply keeps the zone for the same site and
|
|
410
|
+
drops it for another.
|
|
397
411
|
- **Native navigation is the migration target.** Use the shell's native top bar
|
|
398
412
|
and, for bottom-tab navigation, `navigation.tabBar.presentation: "native"`.
|
|
399
413
|
Do not recreate the source Detailed header, call `detailedHeader`, or insert a
|
package/guides/qa-builds.md
CHANGED
|
@@ -643,6 +643,18 @@ the log rather than silently replaced. The sha↔build mapping lands in `artifac
|
|
|
643
643
|
the install page, and the QA summary — the number itself is a clock reading, so it is only traceable
|
|
644
644
|
because those write it down.
|
|
645
645
|
|
|
646
|
+
### Build number
|
|
647
|
+
|
|
648
|
+
A store-bound build can also carry a **store floor**: **`DIST_BUILD_FLOOR_IOS`** /
|
|
649
|
+
**`DIST_BUILD_FLOOR_ANDROID`**, the lowest build number the store will accept for the listing (its
|
|
650
|
+
highest uploaded build + 1). The build machine never reads a store or holds store credentials: the
|
|
651
|
+
floor is an input the job carries. Nothing sets it automatically yet; the store lanes that will read
|
|
652
|
+
the store once per job and pass it are tracked in #4650. When the floor is set, the build number is
|
|
653
|
+
`max(derived, floor)`, and the log says which won (`build 58 (floor: DIST_BUILD_FLOOR_ANDROID 58)` or
|
|
654
|
+
`(derived)`). An explicit `DIST_BUILD_NUMBER` **below** the floor is refused before anything is
|
|
655
|
+
archived, with a message naming the floor; a floor that is not an integer in `1..2100000000` fails
|
|
656
|
+
the job rather than being ignored. A QA (`link`) build gets no floor and is unchanged.
|
|
657
|
+
|
|
646
658
|
## Where an artifact lives, and when it is deleted
|
|
647
659
|
|
|
648
660
|
Each build owns a directory, keyed by the app, the shell, the platform, the shell commit and a
|
|
@@ -5039,6 +5039,79 @@
|
|
|
5039
5039
|
}
|
|
5040
5040
|
]
|
|
5041
5041
|
},
|
|
5042
|
+
{
|
|
5043
|
+
"name": "@himalaya/torch",
|
|
5044
|
+
"source": "core/sdk/torch/src/index.ts",
|
|
5045
|
+
"description": "Tier-5 typed binding for the platform's torch (the camera flashlight).",
|
|
5046
|
+
"api": [
|
|
5047
|
+
{
|
|
5048
|
+
"name": "TorchStatus",
|
|
5049
|
+
"kind": "interface",
|
|
5050
|
+
"jsdoc": "/** The torch right now. `level` is 0..1 while lit, `null` while off or when unknown. */",
|
|
5051
|
+
"decl": "export interface TorchStatus {\n /** Does this device have a torch the app can drive? */\n available: boolean;\n /** Is the torch lit? */\n on: boolean;\n /** Brightness 0..1 while lit (1 = the brightest the device allows), else `null`. */\n level: number | null;\n /** Why the torch is unavailable, when it is. */\n reason?: string;\n}"
|
|
5052
|
+
},
|
|
5053
|
+
{
|
|
5054
|
+
"name": "TorchOnOptions",
|
|
5055
|
+
"kind": "interface",
|
|
5056
|
+
"jsdoc": "",
|
|
5057
|
+
"decl": "export interface TorchOnOptions {\n /** Brightness in 0..1 (default 1). Values outside the range are clamped; 0 turns the torch off.\n * A device without adjustable strength (Android below API 33, or a single-level flash) lights at\n * full brightness and reports `level: 1`. */\n level?: number;\n}"
|
|
5058
|
+
},
|
|
5059
|
+
{
|
|
5060
|
+
"name": "TorchErrorCode",
|
|
5061
|
+
"kind": "type",
|
|
5062
|
+
"jsdoc": "",
|
|
5063
|
+
"decl": "export type TorchErrorCode = \"unavailable\" | \"invalid_argument\" | \"failed\";"
|
|
5064
|
+
},
|
|
5065
|
+
{
|
|
5066
|
+
"name": "TorchError",
|
|
5067
|
+
"kind": "interface",
|
|
5068
|
+
"jsdoc": "/** What every torch verb throws when the native/web arm reports a typed error. */",
|
|
5069
|
+
"decl": "export interface TorchError extends Error {\n code: TorchErrorCode;\n}"
|
|
5070
|
+
},
|
|
5071
|
+
{
|
|
5072
|
+
"name": "torchError",
|
|
5073
|
+
"kind": "function",
|
|
5074
|
+
"jsdoc": "/** Build a [TorchError]. */",
|
|
5075
|
+
"decl": "export function torchError(code: TorchErrorCode, message: string): TorchError;"
|
|
5076
|
+
},
|
|
5077
|
+
{
|
|
5078
|
+
"name": "isTorchError",
|
|
5079
|
+
"kind": "function",
|
|
5080
|
+
"jsdoc": "/** Narrow a caught value: `catch (e) { if (isTorchError(e) && e.code === \"unavailable\") … }`. */",
|
|
5081
|
+
"decl": "export function isTorchError(e: unknown): e is TorchError;"
|
|
5082
|
+
},
|
|
5083
|
+
{
|
|
5084
|
+
"name": "on",
|
|
5085
|
+
"kind": "function",
|
|
5086
|
+
"jsdoc": "/**\n * Turn the torch on, at `level` (0..1, default 1). Resolves with the resulting status.\n * Rejects `unavailable` on a device with no torch, `invalid_argument` for a non-numeric level.\n */",
|
|
5087
|
+
"decl": "export async function on(opts: TorchOnOptions = {}): Promise<TorchStatus>;"
|
|
5088
|
+
},
|
|
5089
|
+
{
|
|
5090
|
+
"name": "off",
|
|
5091
|
+
"kind": "function",
|
|
5092
|
+
"jsdoc": "/** Turn the torch off. Idempotent — resolves (never rejects `unavailable`) when there is no torch. */",
|
|
5093
|
+
"decl": "export async function off(): Promise<TorchStatus>;"
|
|
5094
|
+
},
|
|
5095
|
+
{
|
|
5096
|
+
"name": "toggle",
|
|
5097
|
+
"kind": "function",
|
|
5098
|
+
"jsdoc": "/** Flip the torch: off → on at full brightness, on → off. Rejects `unavailable` with no torch. */",
|
|
5099
|
+
"decl": "export async function toggle(): Promise<TorchStatus>;"
|
|
5100
|
+
},
|
|
5101
|
+
{
|
|
5102
|
+
"name": "status",
|
|
5103
|
+
"kind": "function",
|
|
5104
|
+
"jsdoc": "/** Read the torch's availability and state. Never rejects for a missing torch. */",
|
|
5105
|
+
"decl": "export async function status(): Promise<TorchStatus>;"
|
|
5106
|
+
},
|
|
5107
|
+
{
|
|
5108
|
+
"name": "version",
|
|
5109
|
+
"kind": "const",
|
|
5110
|
+
"jsdoc": "/** Capability version, for bundles that gate on a min binary version. */",
|
|
5111
|
+
"decl": "export const version = 1;"
|
|
5112
|
+
}
|
|
5113
|
+
]
|
|
5114
|
+
},
|
|
5042
5115
|
{
|
|
5043
5116
|
"name": "@himalaya/ui",
|
|
5044
5117
|
"source": "core/sdk/ui/src/index.ts",
|
|
@@ -22598,6 +22671,32 @@
|
|
|
22598
22671
|
"http": "GET /social-groups/v2/rules/{…}",
|
|
22599
22672
|
"scope": "visitor"
|
|
22600
22673
|
},
|
|
22674
|
+
{
|
|
22675
|
+
"name": "WixGroupMembershipStatus",
|
|
22676
|
+
"kind": "type",
|
|
22677
|
+
"jsdoc": "",
|
|
22678
|
+
"decl": "export type WixGroupMembershipStatus = \"UNKNOWN_STATUS\" | \"JOINED\" | \"PENDING\";"
|
|
22679
|
+
},
|
|
22680
|
+
{
|
|
22681
|
+
"name": "WixGroupMembership",
|
|
22682
|
+
"kind": "interface",
|
|
22683
|
+
"jsdoc": "",
|
|
22684
|
+
"decl": "export interface WixGroupMembership {\n groupId?: string;\n status?: WixGroupMembershipStatus | string;\n /** Empty unless the membership is JOINED. */\n role?: { value?: \"MEMBER\" | \"ADMIN\" | string };\n}"
|
|
22685
|
+
},
|
|
22686
|
+
{
|
|
22687
|
+
"name": "WixListMembershipsResponse",
|
|
22688
|
+
"kind": "interface",
|
|
22689
|
+
"jsdoc": "",
|
|
22690
|
+
"decl": "export interface WixListMembershipsResponse {\n memberships?: WixGroupMembership[];\n metadata?: { count?: number; offset?: number; total?: number; tooManyToCount?: boolean };\n}"
|
|
22691
|
+
},
|
|
22692
|
+
{
|
|
22693
|
+
"name": "listMemberships",
|
|
22694
|
+
"kind": "function",
|
|
22695
|
+
"jsdoc": "/**\n * List a member's group memberships — GET /social-groups-proxy/members/v2/members/{memberId}/memberships.\n *\n * @scope member — a signed-in member lists their own memberships only; an owner-scoped token lists\n * anyone's. Sorted by `groupId` descending by default. `limit` is at most 100.\n */",
|
|
22696
|
+
"decl": "export function listMemberships(\n memberId: string,\n paging: { limit?: number; offset?: number } = {},\n opts?: WixCallOptions,\n): Promise<WixListMembershipsResponse>;",
|
|
22697
|
+
"http": "GET /social-groups-proxy/members/v2/members/{…}/memberships${params ?",
|
|
22698
|
+
"scope": "member"
|
|
22699
|
+
},
|
|
22601
22700
|
{
|
|
22602
22701
|
"name": "WixGroupCoverImage",
|
|
22603
22702
|
"kind": "interface",
|
|
@@ -23812,7 +23911,7 @@
|
|
|
23812
23911
|
"name": "WixFollowCounts",
|
|
23813
23912
|
"kind": "interface",
|
|
23814
23913
|
"jsdoc": "",
|
|
23815
|
-
"decl": "export interface WixFollowCounts {\n followers: number;\n following: number;\n}"
|
|
23914
|
+
"decl": "export interface WixFollowCounts {\n /** `null` when Wix returned no total for this direction: unavailable, never an assumed 0. */\n followers: number | null;\n following: number | null;\n}"
|
|
23816
23915
|
},
|
|
23817
23916
|
{
|
|
23818
23917
|
"name": "WixMemberConnection",
|
|
@@ -23829,7 +23928,7 @@
|
|
|
23829
23928
|
{
|
|
23830
23929
|
"name": "getFollowCounts",
|
|
23831
23930
|
"kind": "function",
|
|
23832
|
-
"jsdoc": "/** Followers / following totals for a member — POST\n * /members/v1/activity-counters/query.
|
|
23931
|
+
"jsdoc": "/** Followers / following totals for a member — POST\n * /members/v1/activity-counters/query. Wix keeps ONE counter per direction and creates it on the\n * first change (live, amit1: a followed member's entry holds only `followerCount`, the follower's\n * only `followingCount`), so each direction is reported on its own: an explicit nonnegative safe\n * integer, or `null` when Wix returned none. A counter that is present but malformed makes the\n * whole answer untrusted and rejects, as does an entry with neither counter or a permission\n * failure. Callers hide an unavailable counter; a missing one is never shown as zero. */",
|
|
23833
23932
|
"decl": "export async function getFollowCounts(memberId: string, options?: WixCallOptions): Promise<WixFollowCounts>;",
|
|
23834
23933
|
"http": "POST /members/v1/activity-counters/query"
|
|
23835
23934
|
},
|
|
@@ -52,6 +52,7 @@ the SDK sources, so it matches the shipped SDKs.
|
|
|
52
52
|
- [`system_intents`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fsystem_intents?format=md) — System-intents capability. 2 methods · not in Brandy.
|
|
53
53
|
- [`tasks`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Ftasks?format=md) — Reminders / to-dos. 7 methods.
|
|
54
54
|
- [`theme`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Ftheme?format=md) — Runtime brand overlay. 2 methods.
|
|
55
|
+
- [`torch`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Ftorch?format=md) — Torch (the camera flashlight). 4 methods.
|
|
55
56
|
- [`ui`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fui?format=md) — Imperative popup capability. 2 methods · not in Brandy.
|
|
56
57
|
- [`wix_cashier`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fwix_cashier?format=md) — Native cashier. 2 methods.
|
|
57
58
|
- [`wix_chat`](https://apps.wix.com/_api/himi-serve/authoring/resource/web-sdk%2Fwix_chat?format=md) — Host-owned realtime Chat event subscriptions. 2 methods.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1590.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
|
|
6
6
|
"keywords": [
|
|
@@ -72,5 +72,5 @@
|
|
|
72
72
|
"artifactId": "himalaya-cli"
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
|
-
"falconPackageHash": "
|
|
75
|
+
"falconPackageHash": "0c09c9f148ea94e2a39a974ed0254bee503aa232427deadb20e27979"
|
|
76
76
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// The language a worker formats numbers, money and plurals in, and the business's time zone. A worker has no host locale to read,
|
|
2
|
+
// so the screen's `onAppear` hands it over: `appear()` adds `locale` to the load params when the app
|
|
3
|
+
// pins its language, and `stashCopy` stores it here. Absent, every formatter stays on "en" — the
|
|
4
|
+
// house pattern every English app was approved with — so an unpinned app is unchanged.
|
|
5
|
+
//
|
|
6
|
+
// The business time zone comes the same way, from the app's provisioning (`businessTimeZone`, collected
|
|
7
|
+
// at migration time while the site's properties are readable): a worker cannot read it itself, because
|
|
8
|
+
// the site-properties route is owner-scoped. Absent, it is "" and no worker guesses one.
|
|
9
|
+
//
|
|
10
|
+
// Pure, no host globals: bundled into every worker that formats, copied into generated templates.
|
|
11
|
+
|
|
12
|
+
const TAG = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/;
|
|
13
|
+
let tag = "en";
|
|
14
|
+
|
|
15
|
+
/** Set from a load argument's `locale`; anything that is not a language tag resets to "en". */
|
|
16
|
+
export function setAppLocale(value: unknown): void {
|
|
17
|
+
tag = typeof value === "string" && TAG.test(value) ? value : "en";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function appLocale(): string {
|
|
21
|
+
return tag;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** An IANA zone such as "Asia/Jerusalem" (or "UTC") that this engine knows. A bare offset, a lowercase
|
|
25
|
+
* alias or an engine without ECMA-402 is not one: a zone is never guessed. */
|
|
26
|
+
export function isIanaTimeZone(value: unknown): value is string {
|
|
27
|
+
if (typeof value !== "string" || !/^(?:UTC|[A-Za-z]+(?:\/[A-Za-z0-9_+-]+)+)$/.test(value)) return false;
|
|
28
|
+
// An engine without ECMA-402 has no zone database: nothing it holds is a zone it knows.
|
|
29
|
+
if (typeof Intl === "object" && typeof Intl.DateTimeFormat === "function") {
|
|
30
|
+
try {
|
|
31
|
+
new Intl.DateTimeFormat("en-US", { timeZone: value });
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let zone = "";
|
|
41
|
+
|
|
42
|
+
/** Set from a load argument's `businessTimeZone`; anything that is not an IANA zone clears it. */
|
|
43
|
+
export function setBusinessTimeZone(value: unknown): void {
|
|
44
|
+
zone = isIanaTimeZone(value) ? value : "";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The business's IANA zone, or "" when the app has none provisioned. */
|
|
48
|
+
export function businessTimeZone(): string {
|
|
49
|
+
return zone;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Does `count` take the singular form in the app language? CLDR plural rules, so French "0 point"
|
|
53
|
+
* and "1,5 point" are singular and Hebrew "2" is not. An engine without PluralRules keeps the
|
|
54
|
+
* English rule (exactly one). */
|
|
55
|
+
export function isSingular(count: number, locale = appLocale()): boolean {
|
|
56
|
+
try {
|
|
57
|
+
// An engine without ECMA-402 has no `Intl` at all: it keeps the English rule.
|
|
58
|
+
if (typeof Intl === "object" && typeof Intl.PluralRules === "function") return new Intl.PluralRules(locale).select(count) === "one";
|
|
59
|
+
} catch { /* malformed tag — use the English rule */ }
|
|
60
|
+
return count === 1;
|
|
61
|
+
}
|
|
@@ -98,6 +98,7 @@ export const HOSTED_SCREEN_VERTICALS: Readonly<Record<string, readonly BrandedLi
|
|
|
98
98
|
himi_gift_cards: ["giftCards"],
|
|
99
99
|
himi_groups: ["groups"],
|
|
100
100
|
himi_group_about: ["groups"],
|
|
101
|
+
himi_my_groups: ["groups"],
|
|
101
102
|
himi_invoice: ["invoices"],
|
|
102
103
|
himi_invoices: ["invoices"],
|
|
103
104
|
himi_loyalty: ["loyalty"],
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
// These helpers are the sanctioned way to format across the whole fleet: real ECMA-402 where it
|
|
21
21
|
// exists, a deterministic en-US-shaped fallback where it does not, and never a throw.
|
|
22
22
|
|
|
23
|
+
import { appLocale } from "./app-locale.js";
|
|
24
|
+
|
|
23
25
|
/** Is ECMA-402 actually present in THIS engine? Cheap, and safe in a realm without `Intl`. */
|
|
24
26
|
export function hasIntl(): boolean {
|
|
25
27
|
return typeof Intl !== "undefined";
|
|
@@ -189,7 +191,7 @@ const CURRENCY_DIGITS: Record<string, number> = {
|
|
|
189
191
|
export function formatCurrency(
|
|
190
192
|
amount: number,
|
|
191
193
|
currency = "USD",
|
|
192
|
-
locale =
|
|
194
|
+
locale = appLocale(),
|
|
193
195
|
/** Omit for the currency's natural digits. Pass 0 to render a whole amount as "$25" rather
|
|
194
196
|
* than "$25.00" — Intl clamps the minimum down to match, so max<min does not throw here. */
|
|
195
197
|
maximumFractionDigits?: number,
|
|
@@ -258,7 +260,7 @@ function groupThousands(fixed: string): string {
|
|
|
258
260
|
* inserted commas into the exponent — `1e21` came out as "1e,+21" and `1e-7` as "1,e-7".
|
|
259
261
|
* Garbage, and silently so, because nothing in the corpus's tidy test numbers reached it.
|
|
260
262
|
*/
|
|
261
|
-
export function formatNumber(value: number, locale =
|
|
263
|
+
export function formatNumber(value: number, locale = appLocale()): string {
|
|
262
264
|
if (!Number.isFinite(value)) return "";
|
|
263
265
|
if (hasIntl()) return new Intl.NumberFormat(locale).format(value);
|
|
264
266
|
// `toFixed` itself returns exponential notation at >=1e21, so anything that big keeps the
|
|
@@ -409,7 +411,7 @@ export function compareDateDesc(a: string, b: string): number {
|
|
|
409
411
|
// Customers read that as sloppiness, and a whole "₪70" beside "₪69.50" reads as a different
|
|
410
412
|
// kind of number. So: one money pattern, one clock, one range.
|
|
411
413
|
|
|
412
|
-
// Money uses `formatCurrency(amount, currency
|
|
414
|
+
// Money uses `formatCurrency(amount, currency)` — English unless the app pins another language, see app-locale — the currency's NATURAL decimals
|
|
413
415
|
// ("₪70.00", "¥500") — for Shop, Menu and every other price. The one exception is a Bookings
|
|
414
416
|
// SERVICE price (`_shared/wix-service-price.ts`): it drops the cents of a whole amount ("₪70",
|
|
415
417
|
// "₪69.50"), the format the redesigned service screen and services list were approved with. It
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { copyPlural } from "./himi-copy.js";
|
|
2
|
+
|
|
3
|
+
/** "840 members" / "1 member" for a group, through the copy pack so a translated app reads its own
|
|
4
|
+
* language. One key for every worker that shows the count: a screen that renders it must list the
|
|
5
|
+
* `groups.` prefix in its `appear()` so the pack carries the key. */
|
|
6
|
+
export function groupMemberCount(count: number): string {
|
|
7
|
+
return copyPlural("groups.memberCount", "{n, plural, one {# member} other {# members}}", count);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** The label for a group read back from the offline cache. A cached row keeps the raw `count`, never
|
|
11
|
+
* the formatted label, so a pinned app reads it in its own language. A row written before that (label
|
|
12
|
+
* only) has no count and shows none, instead of a label in the wrong language. */
|
|
13
|
+
export function cachedMemberCount(count: unknown): string {
|
|
14
|
+
return typeof count === "number" && Number.isFinite(count) ? groupMemberCount(count) : "";
|
|
15
|
+
}
|
|
@@ -17,6 +17,7 @@ export function categoryName(value: unknown): string {
|
|
|
17
17
|
return typeof name === "string" ? name.trim() : "";
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
import { businessTimeZone } from "./app-locale.js";
|
|
20
21
|
import { formatDateMedium, formatMonthDay, zoneOffsetAt } from "./engine-intl.js";
|
|
21
22
|
import { formatServiceMoney, formatServicePrice, serviceCurrency } from "./wix-service-price.js";
|
|
22
23
|
|
|
@@ -72,7 +73,8 @@ export function slotIdAt(slots: Iterable<[string, { startDate?: string; timezone
|
|
|
72
73
|
// the button never offers a course that the booking step then refuses.
|
|
73
74
|
|
|
74
75
|
type CoursePolicyShape = {
|
|
75
|
-
|
|
76
|
+
/** `timeZone`: the business's IANA zone, for a session time Wix lists as a bare local wall time. */
|
|
77
|
+
schedule?: { id?: string; firstSessionStart?: string; lastSessionEnd?: string; timeZone?: string };
|
|
76
78
|
bookingPolicy?: {
|
|
77
79
|
bookAfterStartPolicy?: { enabled?: boolean };
|
|
78
80
|
limitEarlyBookingPolicy?: { enabled?: boolean; earliestBookingInMinutes?: number };
|
|
@@ -80,25 +82,63 @@ type CoursePolicyShape = {
|
|
|
80
82
|
};
|
|
81
83
|
};
|
|
82
84
|
|
|
85
|
+
/** A bare local wall time placed in `timeZone`, as an instant (ms). NaN when the value is not a bare local
|
|
86
|
+
* time, has an impossible calendar day, or the engine does not know the zone. */
|
|
87
|
+
function placedInstant(value: string, timeZone: string | undefined): number {
|
|
88
|
+
const local = LOCAL_TIME.exec(value);
|
|
89
|
+
if (HAS_OFFSET.test(value) || !local || !timeZone || !validDate(value)) return NaN;
|
|
90
|
+
const wall = Date.UTC(+local[1], +local[2] - 1, +local[3], +local[4], +local[5], +(local[6] ?? 0));
|
|
91
|
+
const offset = zoneOffsetAt(wall, timeZone);
|
|
92
|
+
if (!Number.isFinite(offset)) return NaN;
|
|
93
|
+
// Once more at the corrected instant, so a wall time beside a DST change lands on the right side.
|
|
94
|
+
const again = zoneOffsetAt(wall - offset, timeZone);
|
|
95
|
+
return wall - (Number.isFinite(again) ? again : offset);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The zone that places a course's bare local session times: the schedule's own, else the app's
|
|
99
|
+
* provisioned business zone. "" when there is none: a zone is never guessed. */
|
|
100
|
+
const courseZone = (schedule: { timeZone?: string } | undefined): string => schedule?.timeZone || businessTimeZone();
|
|
101
|
+
|
|
102
|
+
/** The instant (ms) of a course session time, for the "Ended" label. A time with an offset is exact. A
|
|
103
|
+
* bare local wall time is the BUSINESS's wall time, so it is placed in its zone — never in the device's,
|
|
104
|
+
* which would present another zone's clock as a confirmed instant. No zone, a date-only value, an
|
|
105
|
+
* impossible calendar day or an engine that does not know the zone: unknown (NaN). */
|
|
106
|
+
function sessionInstant(value: string, timeZone: string | undefined): number {
|
|
107
|
+
return HAS_OFFSET.test(value) ? validDate(value)?.getTime() ?? NaN : placedInstant(value, timeZone);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The same instant for the booking rules (window, read range): a bare local time the business zone can
|
|
111
|
+
* place is placed there, so the list label, the service screen and the time screen agree. Everything else
|
|
112
|
+
* keeps the read they always had (`Date.parse`), so an app with no provisioned zone is unchanged. */
|
|
113
|
+
function courseInstant(value: string | undefined, timeZone: string | undefined): number {
|
|
114
|
+
const text = value ?? "";
|
|
115
|
+
const placed = placedInstant(text, timeZone);
|
|
116
|
+
return Number.isFinite(placed) ? placed : Date.parse(text);
|
|
117
|
+
}
|
|
118
|
+
|
|
83
119
|
/** A completed course, independent of whether bookings are currently allowed. Missing,
|
|
84
|
-
* invalid or reversed dates are unknown, never evidence that a course has ended.
|
|
120
|
+
* invalid or reversed dates are unknown, never evidence that a course has ended. A session time
|
|
121
|
+
* without an offset counts only when the schedule names the business's `timeZone`. */
|
|
85
122
|
export function courseHasEnded(service: CoursePolicyShape, now: number): boolean {
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
// in the device's zone and presented as a confirmed business end instant.
|
|
90
|
-
if (!HAS_OFFSET.test(start) || !HAS_OFFSET.test(end)) return false;
|
|
91
|
-
const first = validDate(start)?.getTime() ?? NaN;
|
|
92
|
-
const last = validDate(end)?.getTime() ?? NaN;
|
|
123
|
+
const zone = courseZone(service.schedule);
|
|
124
|
+
const first = sessionInstant(service.schedule?.firstSessionStart ?? "", zone);
|
|
125
|
+
const last = sessionInstant(service.schedule?.lastSessionEnd ?? "", zone);
|
|
93
126
|
return Number.isFinite(now) && Number.isFinite(first) && Number.isFinite(last) && first <= last && last <= now;
|
|
94
127
|
}
|
|
95
128
|
|
|
129
|
+
/** Does this course schedule hold a session time that is a bare local wall time, which only the
|
|
130
|
+
* business's zone can place? A caller reads the zone only when this is true. */
|
|
131
|
+
export function courseNeedsZone(schedule: { firstSessionStart?: string; lastSessionEnd?: string } | undefined): boolean {
|
|
132
|
+
return [schedule?.firstSessionStart, schedule?.lastSessionEnd].some((value) => !!value && !HAS_OFFSET.test(value) && LOCAL_TIME.test(value));
|
|
133
|
+
}
|
|
134
|
+
|
|
96
135
|
/** Whether the business takes course bookings at `now` (ms): the course has not ended, has not
|
|
97
136
|
* started (unless the policy lets people join late), and `now` is inside its booking window —
|
|
98
137
|
* which the early and late limits count from the first session. */
|
|
99
138
|
export function courseWindowOpen(service: CoursePolicyShape, now: number): boolean {
|
|
100
|
-
const
|
|
101
|
-
const
|
|
139
|
+
const zone = courseZone(service.schedule);
|
|
140
|
+
const first = courseInstant(service.schedule?.firstSessionStart, zone);
|
|
141
|
+
const last = courseInstant(service.schedule?.lastSessionEnd, zone);
|
|
102
142
|
if (!Number.isFinite(first) || !Number.isFinite(last) || last <= now) return false;
|
|
103
143
|
const policy = service.bookingPolicy;
|
|
104
144
|
if (first < now && policy?.bookAfterStartPolicy?.enabled !== true) return false;
|
|
@@ -119,8 +159,9 @@ const DAY = 86_400_000;
|
|
|
119
159
|
* its last session (`complete`) and start at its first (`fromFirst`, so a count is the whole
|
|
120
160
|
* course). Empty when the course has no usable dates or has ended. */
|
|
121
161
|
export function courseReadWindows(service: CoursePolicyShape, now: number, chunkDays = 14): { windows: Array<[number, number]>; complete: boolean; fromFirst: boolean } {
|
|
122
|
-
const
|
|
123
|
-
const
|
|
162
|
+
const zone = courseZone(service.schedule);
|
|
163
|
+
const first = courseInstant(service.schedule?.firstSessionStart, zone);
|
|
164
|
+
const last = courseInstant(service.schedule?.lastSessionEnd, zone);
|
|
124
165
|
if (!Number.isFinite(first) || !Number.isFinite(last)) return { windows: [], complete: false, fromFirst: false };
|
|
125
166
|
const from = Math.max(first, now);
|
|
126
167
|
const courseEnd = last + DAY;
|
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
// and every read returns `fallback` verbatim — routing a worker string through
|
|
13
13
|
// copyParam is byte-for-byte behavior-preserving.
|
|
14
14
|
//
|
|
15
|
-
// Pure + Hermes-safe (no host globals, no Node APIs
|
|
15
|
+
// Pure + Hermes-safe (no host globals, no Node APIs; its only import is the pure `app-locale` sibling): bundled into
|
|
16
16
|
// every worker that emits user-facing copy, and copied verbatim into the
|
|
17
17
|
// generated templates' tier5-src/_shared/.
|
|
18
18
|
|
|
19
|
+
import { isSingular, setAppLocale, setBusinessTimeZone } from "./app-locale.js";
|
|
20
|
+
|
|
19
21
|
/** Customer strings override every read; translated stock is valid only for its
|
|
20
22
|
* original English fallback. Some existing keys serve more than one message. */
|
|
21
23
|
export type CopyPackValue = string | { source: string; translation: string };
|
|
@@ -29,6 +31,12 @@ let copyReceived = false;
|
|
|
29
31
|
* stock fallbacks keep rendering. */
|
|
30
32
|
export function stashCopy(arg: unknown): void {
|
|
31
33
|
if (!arg || typeof arg !== "object") return;
|
|
34
|
+
// A param-carrying dispatch names the app language and business zone too (absent: the English house
|
|
35
|
+
// pattern, and no zone).
|
|
36
|
+
if ("copy" in arg || "locale" in arg || "businessTimeZone" in arg) {
|
|
37
|
+
setAppLocale((arg as Record<string, unknown>).locale);
|
|
38
|
+
setBusinessTimeZone((arg as Record<string, unknown>).businessTimeZone);
|
|
39
|
+
}
|
|
32
40
|
const raw = (arg as Record<string, unknown>).copy;
|
|
33
41
|
if (typeof raw !== "string" || !raw) return;
|
|
34
42
|
try {
|
|
@@ -77,5 +85,5 @@ export function copyPlural(key: string, fallback: string, count: number): string
|
|
|
77
85
|
const pattern = copyParam(key, fallback);
|
|
78
86
|
const match = /^\{n, plural, one \{([^{}]*)\} other \{([^{}]*)\}\}$/.exec(pattern);
|
|
79
87
|
if (!match) return copyTemplate(key, fallback, { n: count });
|
|
80
|
-
return (count
|
|
88
|
+
return (isSingular(count) ? match[1] : match[2])!.replace(/#/g, String(count));
|
|
81
89
|
}
|
|
@@ -104,7 +104,7 @@ export function itemPriceLine(item: WixItem): string {
|
|
|
104
104
|
const currency = typeof info?.currency === "string" ? info.currency : "";
|
|
105
105
|
if (!currency) return `${price}`;
|
|
106
106
|
try {
|
|
107
|
-
return formatCurrency(Number(price), currency
|
|
107
|
+
return formatCurrency(Number(price), currency);
|
|
108
108
|
} catch {
|
|
109
109
|
return `${price}`;
|
|
110
110
|
}
|
|
@@ -153,7 +153,7 @@ export function itemPriceVariants(item: WixItem): Array<{ id: string; variantId:
|
|
|
153
153
|
let priceLine = `${price}`;
|
|
154
154
|
if (currency) {
|
|
155
155
|
try {
|
|
156
|
-
priceLine = formatCurrency(Number(price), currency
|
|
156
|
+
priceLine = formatCurrency(Number(price), currency);
|
|
157
157
|
} catch { /* keep the bare amount */ }
|
|
158
158
|
}
|
|
159
159
|
const formatted = typeof node.priceInfo?.formattedPrice === "string" ? node.priceInfo.formattedPrice.trim() : "";
|
|
@@ -74,7 +74,7 @@ export function decimal(value: unknown): number {
|
|
|
74
74
|
|
|
75
75
|
/** Cart V2 money carries no formatted string: format the decimal with the cart's currency. */
|
|
76
76
|
export function money(amount: unknown, currency: string | undefined): string {
|
|
77
|
-
return formatCurrency(decimal(amount), (currency ?? "").trim() || "USD"
|
|
77
|
+
return formatCurrency(decimal(amount), (currency ?? "").trim() || "USD");
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export function cartCurrency(cart: WixCartV2 | undefined): string {
|
|
@@ -111,7 +111,7 @@ export function formatServiceMoney(money: unknown, fallbackCurrency = "USD"): st
|
|
|
111
111
|
if (amount == null) return "";
|
|
112
112
|
const currency = stringOf(m.currency) || fallbackCurrency;
|
|
113
113
|
try {
|
|
114
|
-
return formatCurrency(amount, currency,
|
|
114
|
+
return formatCurrency(amount, currency, undefined, Number.isInteger(amount) ? 0 : undefined);
|
|
115
115
|
} catch {
|
|
116
116
|
return `${amount}`;
|
|
117
117
|
}
|
|
@@ -139,7 +139,7 @@ export function formatServicePrice(service: unknown): string {
|
|
|
139
139
|
try {
|
|
140
140
|
// A whole amount drops its cents ("₪70", not "₪70.00"), as formatServiceMoney and the service
|
|
141
141
|
// screen show it, so the same service reads the same price on every screen.
|
|
142
|
-
return formatCurrency(amount, currency,
|
|
142
|
+
return formatCurrency(amount, currency, undefined, Number.isInteger(amount) ? 0 : undefined);
|
|
143
143
|
} catch {
|
|
144
144
|
return `${amount}`;
|
|
145
145
|
}
|
|
@@ -14,7 +14,8 @@ import { defineService } from "@himalaya/state";
|
|
|
14
14
|
import { defineBackendCollection } from "../_shared/collection-service.js";
|
|
15
15
|
import { COLLECTION_CHROME_MIN_ITEMS } from "../_shared/collection-contract.js";
|
|
16
16
|
import { queryServiceCategories, queryServices, type WixService } from "@himalaya/wix-bookings";
|
|
17
|
-
import {
|
|
17
|
+
import { businessTimeZone } from "../_shared/app-locale.js";
|
|
18
|
+
import { categoryName, courseDatesText, courseHasEnded, courseNeedsZone, serviceDurationText, servicePriceLine, type BookingLabel } from "../_shared/himi-bookings.js";
|
|
18
19
|
import { formatServicePrice } from "../_shared/wix-service-price.js";
|
|
19
20
|
import { getAll, upsert, type DbEntity } from "@himalaya/local-db";
|
|
20
21
|
import { get as getAppInfo } from "@himalaya/app-info";
|
|
@@ -74,7 +75,7 @@ interface Row {
|
|
|
74
75
|
noTypeTag: boolean;
|
|
75
76
|
endedText: string;
|
|
76
77
|
/** Retain course dates so a cached catalog can refresh its status after the course ends. */
|
|
77
|
-
courseSchedule?: { firstSessionStart?: string; lastSessionEnd?: string };
|
|
78
|
+
courseSchedule?: { firstSessionStart?: string; lastSessionEnd?: string; timeZone?: string };
|
|
78
79
|
/** "1 hr · ₪70"; a course: "Nov 3 – Dec 15, 2026 · ₪1,200". "" when the record has neither. */
|
|
79
80
|
meta: string;
|
|
80
81
|
/** Online booking by time is possible: online booking is on and the service is not a course. */
|
|
@@ -269,11 +270,15 @@ function norm(service: WixService): Row {
|
|
|
269
270
|
].filter(Boolean);
|
|
270
271
|
const image = imageUrl(service);
|
|
271
272
|
const isCourse = service.type === "COURSE";
|
|
273
|
+
// A session time Wix lists without an offset is the business's wall time: the schedule carries the
|
|
274
|
+
// provisioned zone that places it (none provisioned: not Ended, never a guess), and the cached row
|
|
275
|
+
// keeps it so an offline list can still age out.
|
|
272
276
|
const courseSchedule = isCourse ? {
|
|
273
277
|
firstSessionStart: service.schedule?.firstSessionStart,
|
|
274
278
|
lastSessionEnd: service.schedule?.lastSessionEnd,
|
|
279
|
+
...(businessTimeZone() && courseNeedsZone(service.schedule) ? { timeZone: businessTimeZone() } : {}),
|
|
275
280
|
} : undefined;
|
|
276
|
-
const endedText =
|
|
281
|
+
const endedText = courseSchedule && courseHasEnded({ schedule: courseSchedule }, Date.now()) ? copyParam("services.ended", "Ended") : "";
|
|
277
282
|
const price = servicePriceLine(service, listLabel).text;
|
|
278
283
|
const meta = [isCourse ? courseDatesText(service) : serviceDurationText(service, listLabel), price].filter(Boolean).join(" · ");
|
|
279
284
|
const typeTag = typeTagOf(service.type);
|
|
@@ -364,7 +364,7 @@ function calculatedDiscount(result: unknown, currency: string): string {
|
|
|
364
364
|
const discount = (result as { summary?: { priceSummary?: { discount?: { amount?: string; convertedAmount?: string } } } } | null)?.summary?.priceSummary?.discount;
|
|
365
365
|
const amount = Number(discount?.amount);
|
|
366
366
|
if (!Number.isFinite(amount) || amount === 0 || !currency) return "";
|
|
367
|
-
return formattedDiscount(formatCurrency(Math.abs(amount), currency
|
|
367
|
+
return formattedDiscount(formatCurrency(Math.abs(amount), currency));
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
function invalidCouponFailure(error: unknown): boolean {
|