@wix/himalaya-cli 0.1538.0 → 0.1540.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/builder.d.ts +5 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/state/src/index.d.ts +7 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +7 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-v1/src/display.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/state.mjs +23 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-events.mjs +1 -0
- package/dist/cli.mjs +2184 -1825
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/component-reference.json +29 -42
- package/guides/component-reference.md +67 -14
- package/guides/dynamic-list.service.ts +7 -1
- package/guides/form-submit.service.ts +7 -1
- package/guides/himi-json-output.md +7 -0
- package/guides/index.json +2 -2
- package/guides/recipe-four-state.md +10 -1
- package/guides/recipe-search.md +9 -7
- package/guides/release-notes.md +17 -0
- package/guides/sdk-reference.json +966 -46
- package/guides/search-list.service.ts +7 -1
- package/guides/templates.md +19 -0
- package/package.json +2 -2
- package/stdlib/design-kits/dark-luxe.json +4 -0
- package/templates/beauty-salon/dev/index.ts +5021 -4164
- package/templates/beauty-salon/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/beauty-salon/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/beauty-salon/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/beauty-salon/tier5-src/_shared/store-order-presentation.ts +56 -0
- package/templates/beauty-salon/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/beauty-salon/tier5-src/himi-branded-lite-category/index.ts +196 -0
- package/templates/beauty-salon/tier5-src/himi-branded-lite-order/index.ts +29 -6
- package/templates/beauty-salon/tier5-src/himi-branded-lite-orders/index.ts +58 -5
- package/templates/beauty-salon/tier5-src/wix-spaces-collections/index.ts +37 -88
- package/templates/beauty-salon-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/beauty-salon-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/branded-lite/dev/index.ts +4933 -4079
- package/templates/branded-lite/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/branded-lite/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/branded-lite/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/branded-lite/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/branded-lite/tier5-src/_shared/store-order-presentation.ts +56 -0
- package/templates/branded-lite/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-category/index.ts +196 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-order/index.ts +29 -6
- package/templates/branded-lite/tier5-src/himi-branded-lite-orders/index.ts +58 -5
- package/templates/branded-lite/tier5-src/wix-spaces-collections/index.ts +37 -88
- package/templates/branded-lite/tokens.json +1 -1
- package/templates/brandy/tokens.json +1 -1
- package/templates/community-club/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/community-club/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/community-club/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/community-club/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/community-club/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/community-club-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/community-club-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/events-meetups/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/events-meetups/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/events-meetups/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/events-meetups/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/events-meetups/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/events-meetups-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/events-meetups-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/gym-fitness/dev/index.ts +5018 -4161
- package/templates/gym-fitness/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/gym-fitness/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/gym-fitness/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/gym-fitness/tier5-src/_shared/store-order-presentation.ts +56 -0
- package/templates/gym-fitness/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/gym-fitness/tier5-src/himi-branded-lite-category/index.ts +196 -0
- package/templates/gym-fitness/tier5-src/himi-branded-lite-order/index.ts +29 -6
- package/templates/gym-fitness/tier5-src/himi-branded-lite-orders/index.ts +58 -5
- package/templates/gym-fitness/tier5-src/wix-spaces-collections/index.ts +37 -88
- package/templates/gym-fitness-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/gym-fitness-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/nonprofit-cause/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/nonprofit-cause/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/nonprofit-cause/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/nonprofit-cause/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/nonprofit-cause-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/owner-lite/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/owner-lite/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/owner-lite/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/podcast-creator/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/podcast-creator/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/podcast-creator/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/podcast-creator/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/podcast-creator/tokens.json +4 -4
- package/templates/podcast-creator-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/podcast-creator-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/podcast-creator-owner/tokens.json +4 -4
- package/templates/portfolio-creative/dev/index.ts +5017 -4157
- package/templates/portfolio-creative/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/portfolio-creative/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/portfolio-creative/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/portfolio-creative/tier5-src/_shared/store-order-presentation.ts +56 -0
- package/templates/portfolio-creative/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-category/index.ts +196 -0
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-order/index.ts +29 -6
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-orders/index.ts +58 -5
- package/templates/portfolio-creative/tier5-src/wix-spaces-collections/index.ts +37 -88
- package/templates/portfolio-creative-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/portfolio-creative-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/restaurant/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/restaurant/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/restaurant/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/restaurant/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/restaurant/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/restaurant/tokens.json +4 -4
- package/templates/restaurant-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/restaurant-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/retail-store/dev/index.ts +5159 -4302
- package/templates/retail-store/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/retail-store/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/retail-store/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/retail-store/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/retail-store/tier5-src/_shared/store-order-presentation.ts +56 -0
- package/templates/retail-store/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/retail-store/tier5-src/himi-branded-lite-category/index.ts +196 -0
- package/templates/retail-store/tier5-src/himi-branded-lite-order/index.ts +29 -6
- package/templates/retail-store/tier5-src/himi-branded-lite-orders/index.ts +58 -5
- package/templates/retail-store/tier5-src/wix-spaces-collections/index.ts +37 -88
- package/templates/retail-store-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/retail-store-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/yoga-studio/dev/index.ts +0 -3
- package/templates/yoga-studio/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/yoga-studio/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/yoga-studio/tier5-src/_shared/himi-store-categories.ts +18 -12
- package/templates/yoga-studio/tier5-src/_shared/wix-service-price.ts +4 -2
- package/templates/yoga-studio-owner/tier5-src/_shared/collection-contract.ts +9 -3
- package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +6 -3
- package/templates/yoga-studio-owner/tier5-src/_shared/engine-intl.ts +5 -2
- package/templates/beauty-salon/tier5-src/wix-spaces-collection-detail/index.ts +0 -170
- package/templates/branded-lite/tier5-src/wix-spaces-collection-detail/index.ts +0 -170
- package/templates/gym-fitness/tier5-src/wix-spaces-collection-detail/index.ts +0 -170
- package/templates/portfolio-creative/tier5-src/wix-spaces-collection-detail/index.ts +0 -170
- package/templates/retail-store/tier5-src/wix-spaces-collection-detail/index.ts +0 -170
|
@@ -2,6 +2,8 @@ import { compareText, compareIso } from "./engine-intl.js";
|
|
|
2
2
|
/** Shared OTA collection contract. No host APIs: used by workers and builders. */
|
|
3
3
|
export interface CollectionConfig {
|
|
4
4
|
rows: string;
|
|
5
|
+
/** Flat customer history rows, divided rather than boxed. */
|
|
6
|
+
rowStyle?: "flat";
|
|
5
7
|
instanceInput?: string;
|
|
6
8
|
title: string;
|
|
7
9
|
subtitle?: string;
|
|
@@ -84,7 +86,7 @@ export const COLLECTIONS: Record<string, CollectionConfig> = {
|
|
|
84
86
|
himiBrandedGroups: { rows: 'rows', title: 'name', subtitle: 'description', image: 'image', view: 'grid' },
|
|
85
87
|
himiBrandedCms: { rows: 'rows', title: 'name', subtitle: 'subtitle', view: 'grid' },
|
|
86
88
|
himiBrandedCmsCollection: { instanceInput: 'collection', rows: 'rows', title: 'title', subtitle: 'subtitle', view: 'list' },
|
|
87
|
-
himiBrandedOrders: { rows: 'rows', title: 'title', subtitle: 'subtitle', view: 'list', backendQuery: true,
|
|
89
|
+
himiBrandedOrders: { rows: 'rows', title: 'title', subtitle: 'subtitle', image: 'image', metadata: ['status', 'meta'], rowStyle: 'flat', view: 'list', backendQuery: true,
|
|
88
90
|
searchPlaceholder: 'Search by order number', countNoun: ['order', 'orders'], filterValues: ['Not fulfilled', 'Partially fulfilled', 'Fulfilled'],
|
|
89
91
|
sortLabels: { default: 'Newest', az: 'Oldest', za: 'Order # ↓' } },
|
|
90
92
|
himiBrandedPlans: { rows: 'rows', title: 'name', subtitle: 'description', metadata: ['badge', 'priceLine', 'periodLine', 'trialBadge', 'perk1', 'perk2', 'perk3', 'joinNote'], view: 'grid', gridColumns: 1, metaMaxLines: 8, countNoun: ['plan', 'plans'] },
|
|
@@ -139,7 +141,10 @@ export function normalizeCollection(config: CollectionConfig, value: unknown): C
|
|
|
139
141
|
density: s.density === 'compact' ? 'compact' : 'comfortable',
|
|
140
142
|
sort: s.sort === 'az' || s.sort === 'za' ? s.sort : 'default',
|
|
141
143
|
query: typeof s.query === 'string' ? s.query.slice(0, 256) : '',
|
|
142
|
-
|
|
144
|
+
// Flat collections no longer offer Saved. Migrate its persisted value before
|
|
145
|
+
// the first backend request so an invisible old filter cannot empty the list.
|
|
146
|
+
filter: config.rowStyle === 'flat' && s.filter === 'saved' ? 'all'
|
|
147
|
+
: typeof s.filter === 'string' ? s.filter.slice(0, 256) : config.defaultFilter ?? 'all',
|
|
143
148
|
savedIds: Array.isArray(s.savedIds) ? [...new Set(s.savedIds.filter((id): id is string => typeof id === 'string' && id.length > 0 && id.length <= 256))].slice(0, 500) : [],
|
|
144
149
|
savedViews: Array.isArray(s.savedViews) ? s.savedViews.filter((v): v is SavedCollectionView => !!v && typeof v === 'object' && typeof v.name === 'string' && !!v.name.trim()).slice(0, 8).map((v, i) => ({
|
|
145
150
|
id: `view-${i}`, name: v.name.trim().slice(0, 40),
|
|
@@ -147,7 +152,8 @@ export function normalizeCollection(config: CollectionConfig, value: unknown): C
|
|
|
147
152
|
density: v.density === 'compact' ? 'compact' : 'comfortable',
|
|
148
153
|
sort: v.sort === 'az' || v.sort === 'za' ? v.sort : 'default',
|
|
149
154
|
query: typeof v.query === 'string' ? v.query.slice(0, 256) : '',
|
|
150
|
-
filter:
|
|
155
|
+
filter: config.rowStyle === 'flat' && v.filter === 'saved' ? 'all'
|
|
156
|
+
: typeof v.filter === 'string' ? v.filter.slice(0, 256) : 'all',
|
|
151
157
|
})) : [],
|
|
152
158
|
};
|
|
153
159
|
}
|
|
@@ -95,7 +95,10 @@ export function decorateCollection(name: string, def: { state: State; actions?:
|
|
|
95
95
|
if (sort === 'default' || sort === 'az' || sort === 'za') { prefs.sort = sort; changed = true; }
|
|
96
96
|
}
|
|
97
97
|
if (Object.prototype.hasOwnProperty.call(input, 'collectionFilter')) {
|
|
98
|
-
const
|
|
98
|
+
const requestedFilter = String(input.collectionFilter ?? '');
|
|
99
|
+
// Old links to Saved must migrate with the stored preference when this
|
|
100
|
+
// collection no longer exposes that filter.
|
|
101
|
+
const filter = config.rowStyle === 'flat' && requestedFilter === 'saved' ? 'all' : requestedFilter;
|
|
99
102
|
const configured = config.filterValues ?? [];
|
|
100
103
|
const rows = Array.isArray(state[config.rows]) ? state[config.rows] as CollectionRow[] : [];
|
|
101
104
|
const derived = config.filter ? rows.map(row => String(row[config.filter!] ?? '')).filter(Boolean) : [];
|
|
@@ -139,7 +142,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
|
|
|
139
142
|
copyParam('collection.itemActionsFor', 'Actions for {title}'), copyParam('collection.itemActions', 'Item actions'));
|
|
140
143
|
});
|
|
141
144
|
if (previewId && !rows.some(row => collectionId(row) === previewId)) previewId = '';
|
|
142
|
-
const filterChoices = [{ id: 'all', label: copyParam('collection.all', 'All') }, { id: 'saved', label: copyParam('collection.saved', 'Saved') },
|
|
145
|
+
const filterChoices = [{ id: 'all', label: copyParam('collection.all', 'All') }, ...(config.rowStyle === 'flat' ? [] : [{ id: 'saved', label: copyParam('collection.saved', 'Saved') }]),
|
|
143
146
|
...projected.filters.map(label => ({ id: `value:${label}`, label: copyParam(`collection.filter.${label}`, label) }))];
|
|
144
147
|
const loading = s.status === 'loading' || s.isLoading === true;
|
|
145
148
|
// Older domain reducers expose `errored` rather than `isErrored`; backend
|
|
@@ -220,7 +223,7 @@ export function decorateCollection(name: string, def: { state: State; actions?:
|
|
|
220
223
|
? [copyParam(`collection.${name}.countOne`, config.countNoun[0]), copyParam(`collection.${name}.countMany`, config.countNoun[1])] : undefined }),
|
|
221
224
|
selectedSummary: `${prefs.selectedIds.length} ${copyParam('collection.selected', 'selected')}`,
|
|
222
225
|
filterChoices,
|
|
223
|
-
chips: config.filterValues ? filterChoices.filter(choice => ['all', 'saved', prefs.filter].includes(choice.id)) : filterChoices,
|
|
226
|
+
chips: config.filterValues && config.rowStyle !== 'flat' ? filterChoices.filter(choice => ['all', 'saved', prefs.filter].includes(choice.id)) : filterChoices,
|
|
224
227
|
} };
|
|
225
228
|
};
|
|
226
229
|
const actions: Record<string, Reducer> = {};
|
|
@@ -410,8 +410,11 @@ export function compareDateDesc(a: string, b: string): number {
|
|
|
410
410
|
// kind of number. So: one money pattern, one clock, one range.
|
|
411
411
|
|
|
412
412
|
// Money uses `formatCurrency(amount, currency, "en")` — the currency's NATURAL decimals
|
|
413
|
-
// ("₪70.00", "¥500")
|
|
414
|
-
//
|
|
413
|
+
// ("₪70.00", "¥500") — for Shop, Menu and every other price. The one exception is a Bookings
|
|
414
|
+
// SERVICE price (`_shared/wix-service-price.ts`): it drops the cents of a whole amount ("₪70",
|
|
415
|
+
// "₪69.50"), the format the redesigned service screen and services list were approved with. It
|
|
416
|
+
// must read the same on every screen that shows a service (the list, the service screen, the
|
|
417
|
+
// time screen, Home, the agenda); do not put those back on natural decimals one screen at a time.
|
|
415
418
|
|
|
416
419
|
const KNOWN_SYMBOLS = new Set(Object.values(CURRENCY_SYMBOL));
|
|
417
420
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Catalog V3 Categories first, V1 collections only when V3 THROWS (a legacy V1 site):
|
|
5
5
|
// a V3 success with zero rows is an empty catalogue, and V1 on a V3 site answers 428.
|
|
6
6
|
// Same rule as wix-spaces-collections; see the reasoning there.
|
|
7
|
-
import { categoryImage, queryCollections, queryStoreCategories, type StoreCategoriesResponse, type StoreCategory, type WixCollection } from "@himalaya/wix-stores-collections";
|
|
7
|
+
import { categoryCount, categoryImage, queryCollections, queryStoreCategories, type StoreCategoriesResponse, type StoreCategory, type WixCollection } from "@himalaya/wix-stores-collections";
|
|
8
8
|
import { wixMediaUrl } from "@himalaya/wix-media";
|
|
9
9
|
|
|
10
10
|
export interface StoreCategoryCard {
|
|
@@ -13,6 +13,7 @@ export interface StoreCategoryCard {
|
|
|
13
13
|
image: string;
|
|
14
14
|
hasImage: boolean;
|
|
15
15
|
noImage: boolean;
|
|
16
|
+
subtitle?: string;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
/** "All Products" is a Wix system collection — every product is in it, so it is
|
|
@@ -20,12 +21,12 @@ export interface StoreCategoryCard {
|
|
|
20
21
|
const SYSTEM_NAMES = ["all products"];
|
|
21
22
|
const SYSTEM_COLLECTION_IDS = ["00000000-000000-000000-000000000001"];
|
|
22
23
|
|
|
23
|
-
function card(id: string, name: string, image: string): StoreCategoryCard {
|
|
24
|
-
return { id, name, image, hasImage: !!image, noImage: !image };
|
|
24
|
+
function card(id: string, name: string, image: string, subtitle = ""): StoreCategoryCard {
|
|
25
|
+
return { id, name, image, hasImage: !!image, noImage: !image, subtitle };
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
function fromCategory(c: StoreCategory): StoreCategoryCard {
|
|
28
|
-
return card(c.id ?? "", (c.name ?? "").trim(), categoryImage(c));
|
|
29
|
+
return card(c.id ?? "", (c.name ?? "").trim(), categoryImage(c), categoryCount(c));
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
function fromCollection(c: WixCollection): StoreCategoryCard {
|
|
@@ -34,19 +35,24 @@ function fromCollection(c: WixCollection): StoreCategoryCard {
|
|
|
34
35
|
return card(raw.id ?? "", (raw.name ?? "").trim(), wixMediaUrl(media?.image?.url ?? media?.thumbnail?.url ?? ""));
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
function visible(rows: StoreCategoryCard[]): StoreCategoryCard[] {
|
|
38
|
+
function visible(rows: StoreCategoryCard[], includeAllProducts = false): StoreCategoryCard[] {
|
|
38
39
|
const seen = new Set<string>();
|
|
39
40
|
return rows.filter((r) => {
|
|
40
|
-
if (!r.id || !r.name || SYSTEM_COLLECTION_IDS.includes(r.id) || SYSTEM_NAMES.includes(r.name.toLowerCase()) || seen.has(r.id)) return false;
|
|
41
|
+
if (!r.id || !r.name || (!includeAllProducts && (SYSTEM_COLLECTION_IDS.includes(r.id) || SYSTEM_NAMES.includes(r.name.toLowerCase()))) || seen.has(r.id)) return false;
|
|
41
42
|
seen.add(r.id);
|
|
42
43
|
return true;
|
|
44
|
+
}).map(row => {
|
|
45
|
+
// Counts belong to the directory opt-in; Home rails and Shop chips retain their
|
|
46
|
+
// established five-field card contract.
|
|
47
|
+
const { subtitle, ...card } = row;
|
|
48
|
+
return includeAllProducts ? row : card;
|
|
43
49
|
});
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
const PAGE_SIZE = 50;
|
|
47
53
|
const MAX_PAGES = 100;
|
|
48
54
|
|
|
49
|
-
async function loadLegacyCategories(): Promise<StoreCategoryCard[]> {
|
|
55
|
+
async function loadLegacyCategories(includeAllProducts: boolean): Promise<StoreCategoryCard[]> {
|
|
50
56
|
const cards: StoreCategoryCard[] = [];
|
|
51
57
|
const seen = new Set<string>();
|
|
52
58
|
let offset = 0;
|
|
@@ -58,7 +64,7 @@ async function loadLegacyCategories(): Promise<StoreCategoryCard[]> {
|
|
|
58
64
|
if (reportedTotal != null) total = Math.max(total ?? 0, reportedTotal);
|
|
59
65
|
if (!rows.length) {
|
|
60
66
|
if (total != null && seen.size < total) throw new Error("Category pagination is incomplete");
|
|
61
|
-
return visible(cards);
|
|
67
|
+
return visible(cards, includeAllProducts);
|
|
62
68
|
}
|
|
63
69
|
const priorCount = seen.size;
|
|
64
70
|
rows.forEach(row => { if (row.id) seen.add(row.id); });
|
|
@@ -67,7 +73,7 @@ async function loadLegacyCategories(): Promise<StoreCategoryCard[]> {
|
|
|
67
73
|
offset += rows.length;
|
|
68
74
|
if (total != null ? offset >= total : rows.length < PAGE_SIZE) {
|
|
69
75
|
if (total != null && seen.size < total) throw new Error("Category pagination is incomplete");
|
|
70
|
-
return visible(cards);
|
|
76
|
+
return visible(cards, includeAllProducts);
|
|
71
77
|
}
|
|
72
78
|
}
|
|
73
79
|
throw new Error("Category pagination exceeded the page limit");
|
|
@@ -75,11 +81,11 @@ async function loadLegacyCategories(): Promise<StoreCategoryCard[]> {
|
|
|
75
81
|
|
|
76
82
|
/** The store's browsable categories in site order. Rejects incomplete reads so callers
|
|
77
83
|
* can retain their last good catalog instead of invalidating a later category. */
|
|
78
|
-
export async function loadStoreCategories(): Promise<StoreCategoryCard[]> {
|
|
84
|
+
export async function loadStoreCategories({ includeAllProducts = false }: { includeAllProducts?: boolean } = {}): Promise<StoreCategoryCard[]> {
|
|
79
85
|
let r: StoreCategoriesResponse;
|
|
80
86
|
try {
|
|
81
87
|
r = await queryStoreCategories(PAGE_SIZE);
|
|
82
|
-
} catch { return loadLegacyCategories(); /* Only the initial failure can identify a legacy V1 site. */ }
|
|
88
|
+
} catch { return loadLegacyCategories(includeAllProducts); /* Only the initial failure can identify a legacy V1 site. */ }
|
|
83
89
|
const cards: StoreCategoryCard[] = [];
|
|
84
90
|
const cursors = new Set<string>();
|
|
85
91
|
const seen = new Set<string>();
|
|
@@ -92,7 +98,7 @@ export async function loadStoreCategories(): Promise<StoreCategoryCard[]> {
|
|
|
92
98
|
const next = r.pagingMetadata?.cursors?.next;
|
|
93
99
|
if (!next) {
|
|
94
100
|
if (seen.size < total) throw new Error("Category pagination is incomplete");
|
|
95
|
-
return visible(cards);
|
|
101
|
+
return visible(cards, includeAllProducts);
|
|
96
102
|
}
|
|
97
103
|
if (cursors.has(next)) throw new Error("Category pagination repeated a cursor");
|
|
98
104
|
cursors.add(next);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Display-only fields from the ecom Order wire shape. No additional endpoints or grants.
|
|
2
|
+
// The generated minimal WixOrder intentionally omits these optional fields.
|
|
3
|
+
import type { WixOrder, WixOrderLineItem, WixOrderLineItemDescriptionLine } from "@himalaya/wix-ecom";
|
|
4
|
+
import { copyParam } from "./himi-copy.js";
|
|
5
|
+
interface Address {
|
|
6
|
+
addressLine?: string;
|
|
7
|
+
addressLine2?: string;
|
|
8
|
+
streetAddress?: { number?: string; name?: string };
|
|
9
|
+
city?: string;
|
|
10
|
+
subdivision?: string;
|
|
11
|
+
subdivisionFullname?: string;
|
|
12
|
+
postalCode?: string;
|
|
13
|
+
country?: string;
|
|
14
|
+
countryFullname?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface DisplayOrderLine extends WixOrderLineItem {
|
|
17
|
+
descriptionLines?: WixOrderLineItemDescriptionLine[];
|
|
18
|
+
}
|
|
19
|
+
export interface DisplayOrder extends WixOrder {
|
|
20
|
+
shippingInfo?: {
|
|
21
|
+
title?: string;
|
|
22
|
+
logistics?: {
|
|
23
|
+
shippingDestination?: { address?: Address; contactDetails?: { firstName?: string; lastName?: string } };
|
|
24
|
+
pickupDetails?: { address?: Address };
|
|
25
|
+
deliveryTime?: string;
|
|
26
|
+
instructions?: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function orderDelivery(order: DisplayOrder): { deliveryTitle: string; deliveryLines: string; deliveryMethod: string; deliveryAddress: string } {
|
|
31
|
+
const info = order.shippingInfo;
|
|
32
|
+
const logistics = info?.logistics;
|
|
33
|
+
const pickup = logistics?.pickupDetails;
|
|
34
|
+
const destination = logistics?.shippingDestination;
|
|
35
|
+
const address = pickup?.address ?? destination?.address;
|
|
36
|
+
const name = pickup ? "" : [destination?.contactDetails?.firstName, destination?.contactDetails?.lastName].filter(Boolean).join(" ");
|
|
37
|
+
const street = address?.addressLine || [address?.streetAddress?.number, address?.streetAddress?.name].filter(Boolean).join(" ");
|
|
38
|
+
const addressLines = [name, street, address?.addressLine2,
|
|
39
|
+
[address?.city, address?.subdivisionFullname || address?.subdivision, address?.postalCode].filter(Boolean).join(", "),
|
|
40
|
+
address?.countryFullname || address?.country,
|
|
41
|
+
].filter(Boolean);
|
|
42
|
+
const methodLines = [info?.title, logistics?.deliveryTime, logistics?.instructions].filter(Boolean);
|
|
43
|
+
return {
|
|
44
|
+
deliveryTitle: pickup ? copyParam("order.pickupTitle", "Pickup") : copyParam("order.deliveryTitle", "Delivery"),
|
|
45
|
+
deliveryLines: [info?.title, ...addressLines, logistics?.deliveryTime, logistics?.instructions].filter(Boolean).join("\n"),
|
|
46
|
+
deliveryMethod: methodLines.join("\n"),
|
|
47
|
+
deliveryAddress: addressLines.join("\n"),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function orderOptions(line: DisplayOrderLine): string {
|
|
51
|
+
return (line.descriptionLines ?? []).map(option => {
|
|
52
|
+
const name = option.name?.translated || option.name?.original;
|
|
53
|
+
const value = option.plainText?.translated || option.plainText?.original;
|
|
54
|
+
return value ? [name, value].filter(Boolean).join(": ") : "";
|
|
55
|
+
}).filter(Boolean).join(" · ");
|
|
56
|
+
}
|
|
@@ -118,7 +118,7 @@ export function formatServiceMoney(money: unknown, fallbackCurrency = "USD"): st
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/**
|
|
121
|
-
* Format a Wix Bookings service's price for display, e.g. "₪111" / "$20".
|
|
121
|
+
* Format a Wix Bookings service's price for display, e.g. "₪111" / "$20" / "$19.50".
|
|
122
122
|
*
|
|
123
123
|
* Reads, in order of preference:
|
|
124
124
|
* 1. Bookings v2 fixed price — `payment.fixed.price.{value,currency}` (the
|
|
@@ -137,7 +137,9 @@ export function formatServicePrice(service: unknown): string {
|
|
|
137
137
|
if (amount == null) return "";
|
|
138
138
|
const currency = serviceCurrency(service);
|
|
139
139
|
try {
|
|
140
|
-
|
|
140
|
+
// A whole amount drops its cents ("₪70", not "₪70.00"), as formatServiceMoney and the service
|
|
141
|
+
// screen show it, so the same service reads the same price on every screen.
|
|
142
|
+
return formatCurrency(amount, currency, "en", Number.isInteger(amount) ? 0 : undefined);
|
|
141
143
|
} catch {
|
|
142
144
|
return `${amount}`;
|
|
143
145
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// A category-scoped Shop gallery. Reuses Shop's card projection, filters, saved
|
|
2
|
+
// products, view preferences and App Store price presentation; the route remains
|
|
3
|
+
// himi_collection { id }. Never substitute the full catalog for a missing category.
|
|
4
|
+
import { getCollection, getStoreCategory, categoryCount } from "@himalaya/wix-stores-collections";
|
|
5
|
+
import { searchStoreProducts, type StoreProduct } from "../_shared/himi-store.js";
|
|
6
|
+
import { createCollectionQuery } from "../_shared/collection-query.js";
|
|
7
|
+
import { detailInputId, presence, presenceAfterError, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
8
|
+
import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
|
|
9
|
+
import { FACETS_BOOT, PAGE_SIZE, applyFacets, currencyOf, facetActions, idOf, readViewPrefs, toRows, toggleSaved, type FacetState } from "../_shared/himi-store-facets.js";
|
|
10
|
+
import { withIapPresentation } from "../_shared/iap-presentation.js";
|
|
11
|
+
|
|
12
|
+
interface State extends FacetState, PresenceFlags {
|
|
13
|
+
name: string;
|
|
14
|
+
subtitle: string;
|
|
15
|
+
description: string;
|
|
16
|
+
query: string;
|
|
17
|
+
loadId: number;
|
|
18
|
+
countLabel: string;
|
|
19
|
+
noEmpty: boolean;
|
|
20
|
+
emptyText: string;
|
|
21
|
+
notLoading: boolean;
|
|
22
|
+
isLoading: boolean;
|
|
23
|
+
notErrored: boolean;
|
|
24
|
+
noPageError: boolean;
|
|
25
|
+
pendingProducts: boolean;
|
|
26
|
+
noRefreshError: boolean;
|
|
27
|
+
hasLoadedProducts: boolean;
|
|
28
|
+
}
|
|
29
|
+
let categoryId = "";
|
|
30
|
+
let loadVersion = 0;
|
|
31
|
+
let refreshFailed = false;
|
|
32
|
+
let scanning = false;
|
|
33
|
+
const products = createCollectionQuery<StoreProduct>({
|
|
34
|
+
id: p => p.id ?? "",
|
|
35
|
+
async fetch(_criteria, cursor) {
|
|
36
|
+
const version = loadVersion;
|
|
37
|
+
try {
|
|
38
|
+
const response = await searchStoreProducts({ categoryId, limit: 100, cursor: cursor || undefined });
|
|
39
|
+
return { items: (response.products ?? []).filter(p => !!p.id), nextCursor: response.pagingMetadata?.cursors?.next ?? "", total: response.pagingMetadata?.total };
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (version === loadVersion) refreshFailed = true;
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
}, () => { void svc.productsChanged(); });
|
|
46
|
+
|
|
47
|
+
function derive(s: State): State {
|
|
48
|
+
const page = products.getState();
|
|
49
|
+
const source = s.found ? page.items : [];
|
|
50
|
+
const query = s.query.trim().toLowerCase();
|
|
51
|
+
const rows = toRows(source);
|
|
52
|
+
const next = applyFacets(s, query ? rows.filter(row => row.name.includes(query)) : rows, source.map(currencyOf).find(Boolean));
|
|
53
|
+
const pending = s.found && (page.status === "idle" || page.status === "loading" || page.loadingMore || (!!page.nextCursor && !page.pageError));
|
|
54
|
+
const firstError = s.found && page.status === "error";
|
|
55
|
+
const loaded = s.hasLoadedProducts || page.status === "ready";
|
|
56
|
+
return { ...next,
|
|
57
|
+
results: next.results.map(card => {
|
|
58
|
+
const product = source.find(p => p.id === card.id);
|
|
59
|
+
return { ...card, productNote: [
|
|
60
|
+
product?.inventory?.availabilityStatus === "OUT_OF_STOCK" ? copyParam("collection.outOfStock", "Out of stock")
|
|
61
|
+
: product?.inventory?.availabilityStatus === "PARTIALLY_OUT_OF_STOCK" ? copyParam("collection.someUnavailable", "Some options unavailable") : "",
|
|
62
|
+
product?.productType?.toLowerCase() === "digital" ? copyParam("collection.digitalLabel", "Digital product") : "",
|
|
63
|
+
].filter(Boolean).join(" · ") };
|
|
64
|
+
}),
|
|
65
|
+
priceChips: next.priceChips.map(c => ({ ...c, selected: next.priceSelected.includes(c.id) })),
|
|
66
|
+
opt1Chips: next.opt1Chips.map(c => ({ ...c, selected: next.opt1Selected.includes(c.id) })),
|
|
67
|
+
opt2Chips: next.opt2Chips.map(c => ({ ...c, selected: next.opt2Selected.includes(c.id) })),
|
|
68
|
+
opt3Chips: next.opt3Chips.map(c => ({ ...c, selected: next.opt3Selected.includes(c.id) })),
|
|
69
|
+
countLabel: s.found && page.status === "ready" ? copyTemplate("collection.resultCount", "{count} products", { count: next.matched }) : "",
|
|
70
|
+
// Searching a partially loaded category must not replace its content with
|
|
71
|
+
// a first-load skeleton just because the current pages have no match yet.
|
|
72
|
+
hasLoadedProducts: loaded,
|
|
73
|
+
isLoading: pending && !loaded,
|
|
74
|
+
notLoading: !(pending && !loaded),
|
|
75
|
+
nextCursor: next.nextCursor || (!scanning && !page.pageError ? page.nextCursor : ""),
|
|
76
|
+
noMore: next.noMore && (scanning || !page.nextCursor || page.pageError),
|
|
77
|
+
pendingProducts: pending,
|
|
78
|
+
notErrored: !firstError,
|
|
79
|
+
noPageError: !s.found || !page.pageError,
|
|
80
|
+
noRefreshError: !refreshFailed || !s.found || !loaded || page.pageError,
|
|
81
|
+
// A filtered first page isn't proof of no matches while later pages load.
|
|
82
|
+
noEmpty: !s.found || pending || firstError || page.pageError || next.results.length > 0,
|
|
83
|
+
emptyText: copyParam("collection.productsEmptyBody", "Try another search or browse a different category."),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function readRemaining(version: number): Promise<void> {
|
|
88
|
+
scanning = true;
|
|
89
|
+
let pages = 0;
|
|
90
|
+
try {
|
|
91
|
+
while (version === loadVersion) {
|
|
92
|
+
const page = products.getState();
|
|
93
|
+
if (page.status !== "ready" || page.pageError || !page.nextCursor) return;
|
|
94
|
+
// Leave further pages reachable through the real paging action.
|
|
95
|
+
if (++pages > 100) return;
|
|
96
|
+
await products.loadMore();
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
scanning = false;
|
|
100
|
+
if (version === loadVersion) void svc.productsChanged();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const initial: State = { ...FACETS_BOOT, ...readViewPrefs(), ...presence("loading"),
|
|
104
|
+
name: "", subtitle: "", description: "", query: "", loadId: 0,
|
|
105
|
+
countLabel: "", noEmpty: true, emptyText: "", notLoading: true, isLoading: false, notErrored: true, noPageError: true, pendingProducts: false, noRefreshError: true, hasLoadedProducts: false,
|
|
106
|
+
};
|
|
107
|
+
const facets = facetActions<State>(derive);
|
|
108
|
+
const actions = {
|
|
109
|
+
load: async (s: State, arg?: unknown): Promise<State | undefined> => {
|
|
110
|
+
stashCopy(arg);
|
|
111
|
+
const version = ++loadVersion;
|
|
112
|
+
const id = detailInputId();
|
|
113
|
+
if (!id) return derive({ ...s, ...presence("missing"), loadId: version });
|
|
114
|
+
let header: { name: string; subtitle: string; description: string };
|
|
115
|
+
try {
|
|
116
|
+
try {
|
|
117
|
+
const category = (await getStoreCategory(id)).category;
|
|
118
|
+
if (version !== loadVersion) return undefined;
|
|
119
|
+
if (!category?.id && !category?.name) return derive({ ...s, ...presence("missing"), loadId: version });
|
|
120
|
+
header = { name: category.name ?? "", subtitle: categoryCount(category), description: category.description ?? "" };
|
|
121
|
+
} catch {
|
|
122
|
+
const category = (await getCollection(id)).collection;
|
|
123
|
+
if (version !== loadVersion) return undefined;
|
|
124
|
+
if (!category) return derive({ ...s, ...presence("missing"), loadId: version });
|
|
125
|
+
header = { name: category.name ?? "", subtitle: "", description: category.description ?? "" };
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (version !== loadVersion) return undefined;
|
|
129
|
+
const current = svc.getState() as State;
|
|
130
|
+
// A failed refresh does not establish that an already loaded category is
|
|
131
|
+
// gone. Retain its current search and products, but never carry them to a
|
|
132
|
+
// different route. Successful missing-entity reads above still clear it.
|
|
133
|
+
if (categoryId === id && current.found) {
|
|
134
|
+
refreshFailed = true;
|
|
135
|
+
return derive({ ...current, loadId: version });
|
|
136
|
+
}
|
|
137
|
+
return derive({ ...s, ...presenceAfterError(error), loadId: version });
|
|
138
|
+
}
|
|
139
|
+
if (version !== loadVersion) return undefined;
|
|
140
|
+
const changed = categoryId !== id;
|
|
141
|
+
categoryId = id;
|
|
142
|
+
refreshFailed = false;
|
|
143
|
+
// Settle the first page in THIS load action. Native worker schedulers may
|
|
144
|
+
// defer sidecar reducers until it returns, so a detached first-page fetch
|
|
145
|
+
// cannot be the only path that clears this action's loading snapshot.
|
|
146
|
+
await products.query({ filter: categoryId }, false, true);
|
|
147
|
+
if (version !== loadVersion) return undefined;
|
|
148
|
+
void readRemaining(version);
|
|
149
|
+
return derive({ ...(svc.getState() as State), ...header, ...presence("found"), loadId: version,
|
|
150
|
+
...(changed ? { ...FACETS_BOOT, ...readViewPrefs(), query: "", hasLoadedProducts: false } : {}),
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
productsChanged: (s: State) => s.loadId === loadVersion ? derive(s) : undefined,
|
|
154
|
+
search: (s: State, arg?: { value?: string }) => derive({ ...s, query: (arg?.value ?? "").slice(0, 256), visible: PAGE_SIZE, cursorEpoch: s.cursorEpoch + 1 }),
|
|
155
|
+
toggleSave: (s: State, arg?: unknown) => { try { toggleSaved(s.results, idOf(arg)); } catch { return s; } return derive(s); },
|
|
156
|
+
...facets,
|
|
157
|
+
clearSearch: (s: State) => derive({ ...s, query: "", priceSelected: [], opt1Selected: [], opt2Selected: [], opt3Selected: [], visible: PAGE_SIZE, cursorEpoch: s.cursorEpoch + 1 }),
|
|
158
|
+
toggleFacet: (s: State, arg?: { facet?: string; id?: string }) => {
|
|
159
|
+
const key = arg?.facet;
|
|
160
|
+
if (key !== "price" && key !== "opt1" && key !== "opt2" && key !== "opt3") return s;
|
|
161
|
+
const id = arg?.id;
|
|
162
|
+
if (!id || !s[`${key}Chips`].some(option => option.id === id)) return s;
|
|
163
|
+
const selected = s[`${key}Selected`];
|
|
164
|
+
return derive({ ...s, [`${key}Selected`]: selected.includes(id) ? selected.filter(value => value !== id) : [...selected, id], visible: PAGE_SIZE, cursorEpoch: s.cursorEpoch + 1 });
|
|
165
|
+
},
|
|
166
|
+
loadMore: async (s: State) => {
|
|
167
|
+
if (s.visible < s.matched) return facets.showMore(s);
|
|
168
|
+
await products.loadMore();
|
|
169
|
+
return undefined;
|
|
170
|
+
},
|
|
171
|
+
showMore: async (s: State) => {
|
|
172
|
+
if (s.visible < s.matched) return facets.showMore(s);
|
|
173
|
+
await products.loadMore();
|
|
174
|
+
return undefined;
|
|
175
|
+
},
|
|
176
|
+
retry: async () => { refreshFailed = false; await products.retry(); await readRemaining(loadVersion); return undefined; },
|
|
177
|
+
};
|
|
178
|
+
const register = withIapPresentation<State>({
|
|
179
|
+
prefix: "category",
|
|
180
|
+
transientKeys: Object.keys(initial) as (keyof State & string)[],
|
|
181
|
+
contentActions: Object.keys(actions),
|
|
182
|
+
items: s => s.results.map(row => ({ reference: { kind: "storesProduct", id: row.id }, commerceDisplayPrice: row.commercePrice })),
|
|
183
|
+
present: (s, price, _allowed, apple) => ({ ...s, results: s.results.map(row => {
|
|
184
|
+
const stored = row as typeof row & { commerceWasPrice?: string; commerceOnSale?: boolean };
|
|
185
|
+
const wasPrice = stored.commerceWasPrice ?? row.wasPrice;
|
|
186
|
+
const onSale = stored.commerceOnSale ?? row.onSale;
|
|
187
|
+
return { ...row, price: price(row.id, row.commercePrice) || row.price,
|
|
188
|
+
commerceWasPrice: wasPrice, commerceOnSale: onSale,
|
|
189
|
+
...(apple(row.id) ? { wasPrice: "", onSale: false, notOnSale: true, noTagRow: row.noSwatches }
|
|
190
|
+
: { wasPrice, onSale, notOnSale: !onSale, noTagRow: !onSale && row.noSwatches }),
|
|
191
|
+
};
|
|
192
|
+
}) }),
|
|
193
|
+
});
|
|
194
|
+
const svc = register<State, Record<string, (s: State, arg?: any) => State | undefined | Promise<State | undefined>>>("himiBrandedCategory", { state: initial, actions });
|
|
195
|
+
svc.load();
|
|
196
|
+
export { svc };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { orderDelivery, orderOptions } from "../_shared/store-order-presentation.js";
|
|
1
2
|
import { withOnlineWrites } from "../_shared/online-actions.js";
|
|
2
3
|
import { createIapCommerce, classifyCart } from "@himalaya/iap-checkout";
|
|
3
4
|
import { getStoreProduct } from "../_shared/himi-store.js";
|
|
@@ -32,6 +33,7 @@ interface LineRow {
|
|
|
32
33
|
title: string;
|
|
33
34
|
name: string;
|
|
34
35
|
quantity: string;
|
|
36
|
+
options: string;
|
|
35
37
|
/** Formatted per-line price (fetched with the order; previously discarded). */
|
|
36
38
|
price: string;
|
|
37
39
|
image: string;
|
|
@@ -50,11 +52,21 @@ interface ReorderRef {
|
|
|
50
52
|
|
|
51
53
|
interface State extends LiveSessionFields {
|
|
52
54
|
status: "loading" | "ready" | "error";
|
|
55
|
+
permissionDenied?: boolean;
|
|
56
|
+
hideLoadError?: boolean;
|
|
57
|
+
hidePermissionError?: boolean;
|
|
53
58
|
/** The order this screen is about. Added for #2050: a followed session has to name
|
|
54
59
|
* its record, and until now the id lived only in `load`'s local scope. */
|
|
55
60
|
id: string;
|
|
56
61
|
title: string;
|
|
57
62
|
placedAt: string;
|
|
63
|
+
deliveryTitle: string;
|
|
64
|
+
deliveryLines: string;
|
|
65
|
+
deliveryMethod: string;
|
|
66
|
+
deliveryAddress: string;
|
|
67
|
+
noDeliveryMethod?: boolean;
|
|
68
|
+
noDeliveryAddress?: boolean;
|
|
69
|
+
noDelivery: boolean;
|
|
58
70
|
fulfillmentStatus: string;
|
|
59
71
|
paymentStatus: string;
|
|
60
72
|
subtotal: string;
|
|
@@ -143,10 +155,9 @@ function linePrice(value: unknown): string {
|
|
|
143
155
|
totalPriceAfterTax?: { formattedAmount?: string };
|
|
144
156
|
totalPriceBeforeTax?: { formattedAmount?: string };
|
|
145
157
|
} | undefined;
|
|
146
|
-
return item?.
|
|
147
|
-
?? item?.totalPriceAfterTax?.formattedAmount
|
|
158
|
+
return item?.totalPriceAfterTax?.formattedAmount
|
|
148
159
|
?? item?.totalPriceBeforeTax?.formattedAmount
|
|
149
|
-
?? "";
|
|
160
|
+
?? (item?.price?.formattedAmount ? copyTemplate("order.unitPrice", "{price} each", { price: item.price.formattedAmount }) : "");
|
|
150
161
|
}
|
|
151
162
|
|
|
152
163
|
function lineRows(order: WixOrder): LineRow[] {
|
|
@@ -159,6 +170,7 @@ function lineRows(order: WixOrder): LineRow[] {
|
|
|
159
170
|
name,
|
|
160
171
|
quantity: copyTemplate("order.quantityLine", "Qty {quantity}", { quantity: item.quantity || 1 }),
|
|
161
172
|
price: linePrice(item),
|
|
173
|
+
options: orderOptions(item),
|
|
162
174
|
image,
|
|
163
175
|
initial: (name.trim()[0] || "O").toUpperCase(),
|
|
164
176
|
hasImage: Boolean(image),
|
|
@@ -187,11 +199,16 @@ function derive(state: State): State {
|
|
|
187
199
|
cannotReorder: !canReorder,
|
|
188
200
|
isLoading: state.status === "loading",
|
|
189
201
|
notLoading: state.status !== "loading",
|
|
202
|
+
hideLoadError: state.status !== "error" || state.permissionDenied === true || state.needsSignIn,
|
|
203
|
+
hidePermissionError: state.status !== "error" || state.permissionDenied !== true || state.needsSignIn,
|
|
190
204
|
isErrored: state.status === "error",
|
|
191
205
|
notErrored: state.status !== "error",
|
|
192
|
-
noEmpty: state.status !== "ready" || hasContent,
|
|
206
|
+
noEmpty: state.status !== "ready" || hasContent || state.needsSignIn,
|
|
193
207
|
noSignIn: !state.needsSignIn,
|
|
194
208
|
hideContent: !hasContent,
|
|
209
|
+
noDelivery: !state.deliveryLines,
|
|
210
|
+
noDeliveryMethod: !state.deliveryMethod,
|
|
211
|
+
noDeliveryAddress: !state.deliveryAddress,
|
|
195
212
|
hasShipping: !!state.shipping,
|
|
196
213
|
noShipping: !state.shipping,
|
|
197
214
|
hasTax: !!state.tax,
|
|
@@ -244,6 +261,7 @@ async function findOwnedOrder(id: string, email: string): Promise<WixOrder | und
|
|
|
244
261
|
|
|
245
262
|
function orderState(order: WixOrder): Partial<State> {
|
|
246
263
|
return {
|
|
264
|
+
...orderDelivery(order),
|
|
247
265
|
title: copyTemplate("order.orderTitle", "Order #{number}", { number: order.number || order.id || "" }),
|
|
248
266
|
placedAt: formatDate(order.createdDate),
|
|
249
267
|
fulfillmentStatus: titleCase(order.fulfillmentStatus) || copyParam("order.statusFallback", "Order placed"),
|
|
@@ -260,7 +278,7 @@ function orderState(order: WixOrder): Partial<State> {
|
|
|
260
278
|
let loadVersion = 0;
|
|
261
279
|
let loadedMember: OrderMember | undefined;
|
|
262
280
|
function clearOrder(state: State): State {
|
|
263
|
-
return derive({ ...state, title: "", placedAt: "", fulfillmentStatus: "", paymentStatus: "", subtotal: "", shipping: "", tax: "", discount: "", total: "", rows: [], reorderRefs: [], reorderMessage: "", emptyText: "", needsSignIn: false });
|
|
281
|
+
return derive({ ...state, title: "", placedAt: "", deliveryTitle: "", deliveryLines: "", deliveryMethod: "", deliveryAddress: "", fulfillmentStatus: "", paymentStatus: "", subtotal: "", shipping: "", tax: "", discount: "", total: "", rows: [], reorderRefs: [], reorderMessage: "", emptyText: "", needsSignIn: false, permissionDenied: false });
|
|
264
282
|
}
|
|
265
283
|
async function load(state: State, arg?: unknown): Promise<State | undefined> {
|
|
266
284
|
const version = ++loadVersion;
|
|
@@ -296,7 +314,7 @@ async function load(state: State, arg?: unknown): Promise<State | undefined> {
|
|
|
296
314
|
total: String(cached.subtitle ?? ""), emptyText: copyParam("order.cachedText", "Saved order summary. Connect to refresh details."),
|
|
297
315
|
});
|
|
298
316
|
}
|
|
299
|
-
return finish({ ...state, status: "error", emptyText: orderAccessDenied(error)
|
|
317
|
+
return finish({ ...state, status: "error", permissionDenied: orderAccessDenied(error), emptyText: orderAccessDenied(error)
|
|
300
318
|
? copyParam("order.accessErrorText", "Order details are unavailable for this account.")
|
|
301
319
|
: copyParam("order.loadErrorText", "Order details could not be loaded.") });
|
|
302
320
|
}
|
|
@@ -327,6 +345,11 @@ const svc = defineService<State, {
|
|
|
327
345
|
status: "loading",
|
|
328
346
|
title: "",
|
|
329
347
|
placedAt: "",
|
|
348
|
+
deliveryTitle: "",
|
|
349
|
+
deliveryLines: "",
|
|
350
|
+
deliveryMethod: "",
|
|
351
|
+
deliveryAddress: "",
|
|
352
|
+
noDelivery: true,
|
|
330
353
|
fulfillmentStatus: "",
|
|
331
354
|
paymentStatus: "",
|
|
332
355
|
subtotal: "",
|