@wix/himalaya-cli 0.1498.0 → 0.1500.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/stdlib/wix-sdk/stores-collections/src/display.d.ts +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-collections/src/index.d.ts +2 -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/wix-stores-collections.mjs +2 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-stores.mjs +3 -2
- package/dist/cli.mjs +30 -31
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/trim-a-template.md +1 -1
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +2465 -2035
- package/templates/beauty-salon/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/beauty-salon/tier5-src/_shared/himi-store-facets.ts +478 -0
- package/templates/beauty-salon/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/beauty-salon/tier5-src/himi-branded-lite-shop/index.ts +691 -0
- package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +2 -1
- package/templates/beauty-salon/tier5-src/himi-store-saved/index.ts +1 -0
- package/templates/branded-lite/dev/index.ts +2593 -2163
- package/templates/branded-lite/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/branded-lite/tier5-src/_shared/himi-store-facets.ts +478 -0
- package/templates/branded-lite/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-shop/index.ts +691 -0
- package/templates/branded-lite/tier5-src/himi-store-product/index.ts +2 -1
- package/templates/branded-lite/tier5-src/himi-store-saved/index.ts +1 -0
- package/templates/community-club/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/community-club/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/events-meetups/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/events-meetups/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/gym-fitness/dev/index.ts +2600 -2170
- package/templates/gym-fitness/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/gym-fitness/tier5-src/_shared/himi-store-facets.ts +478 -0
- package/templates/gym-fitness/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/gym-fitness/tier5-src/himi-branded-lite-shop/index.ts +691 -0
- package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +2 -1
- package/templates/gym-fitness/tier5-src/himi-store-saved/index.ts +1 -0
- package/templates/nonprofit-cause/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/nonprofit-cause/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/podcast-creator/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/podcast-creator/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/portfolio-creative/dev/index.ts +2470 -2040
- package/templates/portfolio-creative/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/portfolio-creative/tier5-src/_shared/himi-store-facets.ts +478 -0
- package/templates/portfolio-creative/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-shop/index.ts +691 -0
- package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +2 -1
- package/templates/portfolio-creative/tier5-src/himi-store-saved/index.ts +1 -0
- package/templates/restaurant/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/restaurant/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/retail-store/dev/index.ts +2583 -2137
- package/templates/retail-store/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/retail-store/tier5-src/_shared/himi-store-facets.ts +478 -0
- package/templates/retail-store/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/retail-store/tier5-src/himi-branded-lite-shop/index.ts +691 -0
- package/templates/retail-store/tier5-src/himi-store-product/index.ts +2 -1
- package/templates/retail-store/tier5-src/himi-store-saved/index.ts +1 -0
- package/templates/yoga-studio/tier5-src/_shared/himi-store-categories.ts +102 -0
- package/templates/yoga-studio/tier5-src/_shared/iap-presentation.ts +158 -15
- package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +42 -2
- package/templates/beauty-salon/tier5-src/_shared/himi-store-collection.ts +0 -22
- package/templates/beauty-salon/tier5-src/himi-store-shop/index.ts +0 -173
- package/templates/branded-lite/tier5-src/_shared/himi-store-collection.ts +0 -22
- package/templates/branded-lite/tier5-src/himi-store-shop/index.ts +0 -173
- package/templates/gym-fitness/tier5-src/_shared/himi-store-collection.ts +0 -22
- package/templates/gym-fitness/tier5-src/himi-store-shop/index.ts +0 -173
- package/templates/portfolio-creative/tier5-src/_shared/himi-store-collection.ts +0 -22
- package/templates/portfolio-creative/tier5-src/himi-store-shop/index.ts +0 -173
- package/templates/retail-store/tier5-src/_shared/himi-store-collection.ts +0 -22
- package/templates/retail-store/tier5-src/himi-store-shop/index.ts +0 -173
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
// Tier-5 bundle: himi-store — product catalog.
|
|
2
|
-
import { withIapPresentation } from "../_shared/iap-presentation.js";
|
|
3
|
-
import { storeCollectionSaving } from "../_shared/himi-store-collection.js";
|
|
4
|
-
import { defineBackendCollection } from "../_shared/collection-service.js";
|
|
5
|
-
import { getCurrentCart } from "@himalaya/wix-ecom";
|
|
6
|
-
import { getAll, upsert, type DbEntity } from "@himalaya/local-db";
|
|
7
|
-
import { productImage, productPrice, searchStoreProducts, type StoreProduct } from "../_shared/himi-store.js";
|
|
8
|
-
import { cleanTitle, initials } from "../_shared/himi-text.js";
|
|
9
|
-
import { copyParam } from "../_shared/himi-copy.js";
|
|
10
|
-
|
|
11
|
-
// Offline-first (data-offline): the product catalog is public read-through — paint
|
|
12
|
-
// the last-synced rows from the on-device store first (instant, works offline),
|
|
13
|
-
// then revalidate from Wix and refresh the cache. DEGRADES GRACEFULLY: every
|
|
14
|
-
// local-db call is wrapped so a bundle that ships in an app which hasn't granted
|
|
15
|
-
// `local_database` (+ declared the `products` entity in its dataSchema) just falls
|
|
16
|
-
// back to today's online-only path — the capability fails closed per-entity, so the
|
|
17
|
-
// try/catch is the seam that keeps this shared bundle safe across every consuming app.
|
|
18
|
-
const ENTITY = "products";
|
|
19
|
-
async function readCachedRows(): Promise<Row[]> {
|
|
20
|
-
try { return (await getAll(ENTITY)) as unknown as Row[]; } catch { return []; }
|
|
21
|
-
}
|
|
22
|
-
async function writeCachedRows(rows: Row[]): Promise<void> {
|
|
23
|
-
try { for (const r of rows) await upsert(ENTITY, r as unknown as DbEntity); } catch { /* capability not granted → online-only */ }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Live cart size (sum of line-item quantities) for the top-bar cart badge.
|
|
27
|
-
* Best-effort: a cart error just yields 0 (badge hidden), never fails the screen. */
|
|
28
|
-
async function cartItemCount(): Promise<number> {
|
|
29
|
-
try {
|
|
30
|
-
const cart = await getCurrentCart();
|
|
31
|
-
return (cart.lineItems ?? []).reduce((n, li) => n + (li.quantity ?? 1), 0);
|
|
32
|
-
} catch {
|
|
33
|
-
return 0;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface Row {
|
|
38
|
-
id: string;
|
|
39
|
-
name: string;
|
|
40
|
-
subtitle: string;
|
|
41
|
-
commercePrice: string;
|
|
42
|
-
wasPrice: string;
|
|
43
|
-
image: string;
|
|
44
|
-
initial: string;
|
|
45
|
-
hasImage: boolean;
|
|
46
|
-
noImage: boolean;
|
|
47
|
-
detailScreenId: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface S {
|
|
51
|
-
status: "loading" | "ready" | "error";
|
|
52
|
-
total?: number;
|
|
53
|
-
complete?: boolean;
|
|
54
|
-
pageError?: boolean;
|
|
55
|
-
loadingMore?: boolean;
|
|
56
|
-
fromCache?: boolean;
|
|
57
|
-
query: string;
|
|
58
|
-
all: Row[];
|
|
59
|
-
collection?: { items: Array<Row & { collectionSubtitle: string }> };
|
|
60
|
-
results: Row[];
|
|
61
|
-
/** RFC 0008 cursor paging — non-empty while more pages exist ("" ⇒ exhausted). */
|
|
62
|
-
nextCursor: string;
|
|
63
|
-
emptyText: string;
|
|
64
|
-
countLabel: string;
|
|
65
|
-
cartCount: number;
|
|
66
|
-
isLoading: boolean;
|
|
67
|
-
notLoading: boolean;
|
|
68
|
-
isErrored: boolean;
|
|
69
|
-
notErrored: boolean;
|
|
70
|
-
noEmpty: boolean;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function row(p: StoreProduct): Row {
|
|
74
|
-
const name = cleanTitle(p.name) || "Product";
|
|
75
|
-
const image = productImage(p);
|
|
76
|
-
const id = p.id ?? "";
|
|
77
|
-
const priceData = p.priceData as { formatted?: { discountedPrice?: string; price?: string }; discountedPrice?: string; salePrice?: string } | undefined;
|
|
78
|
-
const regular = productPrice(p);
|
|
79
|
-
const discounted = priceData?.formatted?.discountedPrice ?? priceData?.discountedPrice ?? priceData?.salePrice;
|
|
80
|
-
const salePrice = discounted && discounted !== (priceData?.formatted?.price ?? regular)
|
|
81
|
-
? productPrice({ ...p, priceData: { ...priceData, formatted: { ...priceData?.formatted, price: discounted } } } as StoreProduct)
|
|
82
|
-
: "";
|
|
83
|
-
const originalV3 = p.compareAtPriceRange?.minValue
|
|
84
|
-
? productPrice({ ...p, price: undefined, priceData: undefined, actualPriceRange: p.compareAtPriceRange })
|
|
85
|
-
: "";
|
|
86
|
-
const wasPrice = salePrice ? regular : originalV3 && originalV3 !== regular ? originalV3 : "";
|
|
87
|
-
return {
|
|
88
|
-
id,
|
|
89
|
-
name,
|
|
90
|
-
subtitle: salePrice || regular,
|
|
91
|
-
commercePrice: salePrice || regular,
|
|
92
|
-
wasPrice,
|
|
93
|
-
image,
|
|
94
|
-
initial: initials(name),
|
|
95
|
-
hasImage: !!image,
|
|
96
|
-
noImage: !image,
|
|
97
|
-
// WS-C — canonical param-less detail screenId; the descriptor row binds the
|
|
98
|
-
// separate `id` field as a first-class nav param (no more baked suffixes).
|
|
99
|
-
detailScreenId: "himi_product",
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function derive(s: S): S {
|
|
104
|
-
const count = s.results.length;
|
|
105
|
-
return {
|
|
106
|
-
...s,
|
|
107
|
-
isLoading: s.status === "loading",
|
|
108
|
-
notLoading: s.status !== "loading",
|
|
109
|
-
isErrored: s.status === "error",
|
|
110
|
-
notErrored: s.status !== "error",
|
|
111
|
-
noEmpty: s.status === "loading" || s.status === "error" || count > 0,
|
|
112
|
-
countLabel: count ? `${count} product${count === 1 ? "" : "s"} from Wix Stores` : "",
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
let cacheWrite = Promise.resolve();
|
|
117
|
-
const svc = defineBackendCollection("himiStoreShop", {
|
|
118
|
-
register: withIapPresentation<S>({
|
|
119
|
-
prefix: "shop", transientKeys: ["all", "results", "collection"], contentActions: ["collectionDataChanged"],
|
|
120
|
-
items: state => state.results.map(row => ({ reference: { kind: "storesProduct", id: row.id }, commerceDisplayPrice: row.commercePrice ?? row.subtitle })),
|
|
121
|
-
present: (state, price) => ({ ...state, results: state.results.map(row => ({ ...row, subtitle: price(row.id) })), all: state.all.map(row => ({ ...row, subtitle: price(row.id) })),
|
|
122
|
-
...(state.collection ? { collection: { ...state.collection, items: state.collection.items.map(row => ({ ...row, collectionSubtitle: price(row.id) })) } } : {}) }),
|
|
123
|
-
}),
|
|
124
|
-
state: derive({ status: "loading", query: "", all: [], results: [], nextCursor: "",
|
|
125
|
-
emptyText: "", countLabel: "", cartCount: 0, isLoading: true, notLoading: false,
|
|
126
|
-
isErrored: false, notErrored: true, noEmpty: true }),
|
|
127
|
-
saving: storeCollectionSaving,
|
|
128
|
-
source: {
|
|
129
|
-
id: (item: Row) => item.id,
|
|
130
|
-
async cached(criteria) {
|
|
131
|
-
if (criteria.query || criteria.filter !== "all" || criteria.sort !== "default") return undefined;
|
|
132
|
-
const items = (await readCachedRows()).filter(item => !!item.id);
|
|
133
|
-
return items.length ? { items, nextCursor: "", complete: false, metadata: { fromCache: true } } : undefined;
|
|
134
|
-
},
|
|
135
|
-
async fetch(criteria, cursor) {
|
|
136
|
-
try {
|
|
137
|
-
const response = await searchStoreProducts({ query: criteria.query, sort: criteria.sort,
|
|
138
|
-
...(criteria.filter === "saved" ? { ids: criteria.savedIds } : {}), limit: 50, cursor });
|
|
139
|
-
const items = (response.products ?? []).map(row);
|
|
140
|
-
return { items, nextCursor: response.pagingMetadata?.cursors?.next ?? "",
|
|
141
|
-
total: response.pagingMetadata?.total,
|
|
142
|
-
metadata: cursor ? undefined : { cartCount: await cartItemCount(), fromCache: false } };
|
|
143
|
-
} catch (error) {
|
|
144
|
-
// A cached subset is never passed off as a backend search result.
|
|
145
|
-
const status = /HTTP (\d{3})/.exec(String(error))?.[1];
|
|
146
|
-
if (status !== "401" && status !== "403" && !cursor && !criteria.query && criteria.filter === "all" && criteria.sort === "default") {
|
|
147
|
-
const items = (await readCachedRows()).filter(item => !!item.id);
|
|
148
|
-
if (items.length) return { items, nextCursor: "", complete: false,
|
|
149
|
-
metadata: { fromCache: true } };
|
|
150
|
-
}
|
|
151
|
-
throw error;
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
apply(state, page) {
|
|
156
|
-
if (page.status === "ready" && !page.loadingMore && !page.pageError && !page.metadata?.fromCache) {
|
|
157
|
-
cacheWrite = cacheWrite.then(() => writeCachedRows(page.items));
|
|
158
|
-
}
|
|
159
|
-
const status = page.status === "idle" ? "loading" : page.status;
|
|
160
|
-
return derive({ ...state, ...page.metadata, status, query: page.criteria.query,
|
|
161
|
-
all: page.items, results: page.items, nextCursor: page.nextCursor,
|
|
162
|
-
total: page.total, complete: page.complete, pageError: page.pageError,
|
|
163
|
-
loadingMore: page.loadingMore, fromCache: page.metadata?.fromCache === true,
|
|
164
|
-
emptyText: status === "error" ? copyParam("shop.errorText", "Products could not load.")
|
|
165
|
-
: page.criteria.query || page.criteria.filter !== "all"
|
|
166
|
-
? copyParam("shop.noMatchText", "No products match this view.")
|
|
167
|
-
: copyParam("shop.emptyText", "No products are published yet."),
|
|
168
|
-
});
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
svc.load();
|
|
173
|
-
export { svc };
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { withCollectionSaving, type CollectionSaving } from './collection-service.js';
|
|
2
|
-
import { readSavedProducts, writeSavedProducts } from './himi-store-saved.js';
|
|
3
|
-
|
|
4
|
-
export const storeCollectionSaving: CollectionSaving = {
|
|
5
|
-
read: () => readSavedProducts().map(row => row.id),
|
|
6
|
-
write(ids, rows) {
|
|
7
|
-
const products = new Map(readSavedProducts().map(row => [row.id, row]));
|
|
8
|
-
for (const row of rows) {
|
|
9
|
-
if (typeof row.id !== 'string' || typeof row.name !== 'string') continue;
|
|
10
|
-
const previous = products.get(row.id);
|
|
11
|
-
const freshPrice = typeof row.commercePrice === 'string' ? row.commercePrice : typeof row.subtitle === 'string' ? row.subtitle : '';
|
|
12
|
-
const wasPrice = typeof row.wasPrice === 'string' ? row.wasPrice || undefined : previous?.price === freshPrice ? previous?.wasPrice : undefined;
|
|
13
|
-
products.set(row.id, { ...previous, wasPrice, id: row.id, name: row.name,
|
|
14
|
-
price: freshPrice || previous?.price || '',
|
|
15
|
-
image: typeof row.image === 'string' && row.image ? row.image : previous?.image ?? '' });
|
|
16
|
-
}
|
|
17
|
-
const selected = ids.map(id => products.get(id));
|
|
18
|
-
if (selected.some(row => !row)) throw new Error('Saved product metadata is unavailable');
|
|
19
|
-
writeSavedProducts(selected.filter((row): row is NonNullable<typeof row> => !!row));
|
|
20
|
-
},
|
|
21
|
-
};
|
|
22
|
-
export const defineService = withCollectionSaving(storeCollectionSaving);
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
// Tier-5 bundle: himi-store — product catalog.
|
|
2
|
-
import { withIapPresentation } from "../_shared/iap-presentation.js";
|
|
3
|
-
import { storeCollectionSaving } from "../_shared/himi-store-collection.js";
|
|
4
|
-
import { defineBackendCollection } from "../_shared/collection-service.js";
|
|
5
|
-
import { getCurrentCart } from "@himalaya/wix-ecom";
|
|
6
|
-
import { getAll, upsert, type DbEntity } from "@himalaya/local-db";
|
|
7
|
-
import { productImage, productPrice, searchStoreProducts, type StoreProduct } from "../_shared/himi-store.js";
|
|
8
|
-
import { cleanTitle, initials } from "../_shared/himi-text.js";
|
|
9
|
-
import { copyParam } from "../_shared/himi-copy.js";
|
|
10
|
-
|
|
11
|
-
// Offline-first (data-offline): the product catalog is public read-through — paint
|
|
12
|
-
// the last-synced rows from the on-device store first (instant, works offline),
|
|
13
|
-
// then revalidate from Wix and refresh the cache. DEGRADES GRACEFULLY: every
|
|
14
|
-
// local-db call is wrapped so a bundle that ships in an app which hasn't granted
|
|
15
|
-
// `local_database` (+ declared the `products` entity in its dataSchema) just falls
|
|
16
|
-
// back to today's online-only path — the capability fails closed per-entity, so the
|
|
17
|
-
// try/catch is the seam that keeps this shared bundle safe across every consuming app.
|
|
18
|
-
const ENTITY = "products";
|
|
19
|
-
async function readCachedRows(): Promise<Row[]> {
|
|
20
|
-
try { return (await getAll(ENTITY)) as unknown as Row[]; } catch { return []; }
|
|
21
|
-
}
|
|
22
|
-
async function writeCachedRows(rows: Row[]): Promise<void> {
|
|
23
|
-
try { for (const r of rows) await upsert(ENTITY, r as unknown as DbEntity); } catch { /* capability not granted → online-only */ }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Live cart size (sum of line-item quantities) for the top-bar cart badge.
|
|
27
|
-
* Best-effort: a cart error just yields 0 (badge hidden), never fails the screen. */
|
|
28
|
-
async function cartItemCount(): Promise<number> {
|
|
29
|
-
try {
|
|
30
|
-
const cart = await getCurrentCart();
|
|
31
|
-
return (cart.lineItems ?? []).reduce((n, li) => n + (li.quantity ?? 1), 0);
|
|
32
|
-
} catch {
|
|
33
|
-
return 0;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface Row {
|
|
38
|
-
id: string;
|
|
39
|
-
name: string;
|
|
40
|
-
subtitle: string;
|
|
41
|
-
commercePrice: string;
|
|
42
|
-
wasPrice: string;
|
|
43
|
-
image: string;
|
|
44
|
-
initial: string;
|
|
45
|
-
hasImage: boolean;
|
|
46
|
-
noImage: boolean;
|
|
47
|
-
detailScreenId: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface S {
|
|
51
|
-
status: "loading" | "ready" | "error";
|
|
52
|
-
total?: number;
|
|
53
|
-
complete?: boolean;
|
|
54
|
-
pageError?: boolean;
|
|
55
|
-
loadingMore?: boolean;
|
|
56
|
-
fromCache?: boolean;
|
|
57
|
-
query: string;
|
|
58
|
-
all: Row[];
|
|
59
|
-
collection?: { items: Array<Row & { collectionSubtitle: string }> };
|
|
60
|
-
results: Row[];
|
|
61
|
-
/** RFC 0008 cursor paging — non-empty while more pages exist ("" ⇒ exhausted). */
|
|
62
|
-
nextCursor: string;
|
|
63
|
-
emptyText: string;
|
|
64
|
-
countLabel: string;
|
|
65
|
-
cartCount: number;
|
|
66
|
-
isLoading: boolean;
|
|
67
|
-
notLoading: boolean;
|
|
68
|
-
isErrored: boolean;
|
|
69
|
-
notErrored: boolean;
|
|
70
|
-
noEmpty: boolean;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function row(p: StoreProduct): Row {
|
|
74
|
-
const name = cleanTitle(p.name) || "Product";
|
|
75
|
-
const image = productImage(p);
|
|
76
|
-
const id = p.id ?? "";
|
|
77
|
-
const priceData = p.priceData as { formatted?: { discountedPrice?: string; price?: string }; discountedPrice?: string; salePrice?: string } | undefined;
|
|
78
|
-
const regular = productPrice(p);
|
|
79
|
-
const discounted = priceData?.formatted?.discountedPrice ?? priceData?.discountedPrice ?? priceData?.salePrice;
|
|
80
|
-
const salePrice = discounted && discounted !== (priceData?.formatted?.price ?? regular)
|
|
81
|
-
? productPrice({ ...p, priceData: { ...priceData, formatted: { ...priceData?.formatted, price: discounted } } } as StoreProduct)
|
|
82
|
-
: "";
|
|
83
|
-
const originalV3 = p.compareAtPriceRange?.minValue
|
|
84
|
-
? productPrice({ ...p, price: undefined, priceData: undefined, actualPriceRange: p.compareAtPriceRange })
|
|
85
|
-
: "";
|
|
86
|
-
const wasPrice = salePrice ? regular : originalV3 && originalV3 !== regular ? originalV3 : "";
|
|
87
|
-
return {
|
|
88
|
-
id,
|
|
89
|
-
name,
|
|
90
|
-
subtitle: salePrice || regular,
|
|
91
|
-
commercePrice: salePrice || regular,
|
|
92
|
-
wasPrice,
|
|
93
|
-
image,
|
|
94
|
-
initial: initials(name),
|
|
95
|
-
hasImage: !!image,
|
|
96
|
-
noImage: !image,
|
|
97
|
-
// WS-C — canonical param-less detail screenId; the descriptor row binds the
|
|
98
|
-
// separate `id` field as a first-class nav param (no more baked suffixes).
|
|
99
|
-
detailScreenId: "himi_product",
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function derive(s: S): S {
|
|
104
|
-
const count = s.results.length;
|
|
105
|
-
return {
|
|
106
|
-
...s,
|
|
107
|
-
isLoading: s.status === "loading",
|
|
108
|
-
notLoading: s.status !== "loading",
|
|
109
|
-
isErrored: s.status === "error",
|
|
110
|
-
notErrored: s.status !== "error",
|
|
111
|
-
noEmpty: s.status === "loading" || s.status === "error" || count > 0,
|
|
112
|
-
countLabel: count ? `${count} product${count === 1 ? "" : "s"} from Wix Stores` : "",
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
let cacheWrite = Promise.resolve();
|
|
117
|
-
const svc = defineBackendCollection("himiStoreShop", {
|
|
118
|
-
register: withIapPresentation<S>({
|
|
119
|
-
prefix: "shop", transientKeys: ["all", "results", "collection"], contentActions: ["collectionDataChanged"],
|
|
120
|
-
items: state => state.results.map(row => ({ reference: { kind: "storesProduct", id: row.id }, commerceDisplayPrice: row.commercePrice ?? row.subtitle })),
|
|
121
|
-
present: (state, price) => ({ ...state, results: state.results.map(row => ({ ...row, subtitle: price(row.id) })), all: state.all.map(row => ({ ...row, subtitle: price(row.id) })),
|
|
122
|
-
...(state.collection ? { collection: { ...state.collection, items: state.collection.items.map(row => ({ ...row, collectionSubtitle: price(row.id) })) } } : {}) }),
|
|
123
|
-
}),
|
|
124
|
-
state: derive({ status: "loading", query: "", all: [], results: [], nextCursor: "",
|
|
125
|
-
emptyText: "", countLabel: "", cartCount: 0, isLoading: true, notLoading: false,
|
|
126
|
-
isErrored: false, notErrored: true, noEmpty: true }),
|
|
127
|
-
saving: storeCollectionSaving,
|
|
128
|
-
source: {
|
|
129
|
-
id: (item: Row) => item.id,
|
|
130
|
-
async cached(criteria) {
|
|
131
|
-
if (criteria.query || criteria.filter !== "all" || criteria.sort !== "default") return undefined;
|
|
132
|
-
const items = (await readCachedRows()).filter(item => !!item.id);
|
|
133
|
-
return items.length ? { items, nextCursor: "", complete: false, metadata: { fromCache: true } } : undefined;
|
|
134
|
-
},
|
|
135
|
-
async fetch(criteria, cursor) {
|
|
136
|
-
try {
|
|
137
|
-
const response = await searchStoreProducts({ query: criteria.query, sort: criteria.sort,
|
|
138
|
-
...(criteria.filter === "saved" ? { ids: criteria.savedIds } : {}), limit: 50, cursor });
|
|
139
|
-
const items = (response.products ?? []).map(row);
|
|
140
|
-
return { items, nextCursor: response.pagingMetadata?.cursors?.next ?? "",
|
|
141
|
-
total: response.pagingMetadata?.total,
|
|
142
|
-
metadata: cursor ? undefined : { cartCount: await cartItemCount(), fromCache: false } };
|
|
143
|
-
} catch (error) {
|
|
144
|
-
// A cached subset is never passed off as a backend search result.
|
|
145
|
-
const status = /HTTP (\d{3})/.exec(String(error))?.[1];
|
|
146
|
-
if (status !== "401" && status !== "403" && !cursor && !criteria.query && criteria.filter === "all" && criteria.sort === "default") {
|
|
147
|
-
const items = (await readCachedRows()).filter(item => !!item.id);
|
|
148
|
-
if (items.length) return { items, nextCursor: "", complete: false,
|
|
149
|
-
metadata: { fromCache: true } };
|
|
150
|
-
}
|
|
151
|
-
throw error;
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
apply(state, page) {
|
|
156
|
-
if (page.status === "ready" && !page.loadingMore && !page.pageError && !page.metadata?.fromCache) {
|
|
157
|
-
cacheWrite = cacheWrite.then(() => writeCachedRows(page.items));
|
|
158
|
-
}
|
|
159
|
-
const status = page.status === "idle" ? "loading" : page.status;
|
|
160
|
-
return derive({ ...state, ...page.metadata, status, query: page.criteria.query,
|
|
161
|
-
all: page.items, results: page.items, nextCursor: page.nextCursor,
|
|
162
|
-
total: page.total, complete: page.complete, pageError: page.pageError,
|
|
163
|
-
loadingMore: page.loadingMore, fromCache: page.metadata?.fromCache === true,
|
|
164
|
-
emptyText: status === "error" ? copyParam("shop.errorText", "Products could not load.")
|
|
165
|
-
: page.criteria.query || page.criteria.filter !== "all"
|
|
166
|
-
? copyParam("shop.noMatchText", "No products match this view.")
|
|
167
|
-
: copyParam("shop.emptyText", "No products are published yet."),
|
|
168
|
-
});
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
svc.load();
|
|
173
|
-
export { svc };
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { withCollectionSaving, type CollectionSaving } from './collection-service.js';
|
|
2
|
-
import { readSavedProducts, writeSavedProducts } from './himi-store-saved.js';
|
|
3
|
-
|
|
4
|
-
export const storeCollectionSaving: CollectionSaving = {
|
|
5
|
-
read: () => readSavedProducts().map(row => row.id),
|
|
6
|
-
write(ids, rows) {
|
|
7
|
-
const products = new Map(readSavedProducts().map(row => [row.id, row]));
|
|
8
|
-
for (const row of rows) {
|
|
9
|
-
if (typeof row.id !== 'string' || typeof row.name !== 'string') continue;
|
|
10
|
-
const previous = products.get(row.id);
|
|
11
|
-
const freshPrice = typeof row.commercePrice === 'string' ? row.commercePrice : typeof row.subtitle === 'string' ? row.subtitle : '';
|
|
12
|
-
const wasPrice = typeof row.wasPrice === 'string' ? row.wasPrice || undefined : previous?.price === freshPrice ? previous?.wasPrice : undefined;
|
|
13
|
-
products.set(row.id, { ...previous, wasPrice, id: row.id, name: row.name,
|
|
14
|
-
price: freshPrice || previous?.price || '',
|
|
15
|
-
image: typeof row.image === 'string' && row.image ? row.image : previous?.image ?? '' });
|
|
16
|
-
}
|
|
17
|
-
const selected = ids.map(id => products.get(id));
|
|
18
|
-
if (selected.some(row => !row)) throw new Error('Saved product metadata is unavailable');
|
|
19
|
-
writeSavedProducts(selected.filter((row): row is NonNullable<typeof row> => !!row));
|
|
20
|
-
},
|
|
21
|
-
};
|
|
22
|
-
export const defineService = withCollectionSaving(storeCollectionSaving);
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
// Tier-5 bundle: himi-store — product catalog.
|
|
2
|
-
import { withIapPresentation } from "../_shared/iap-presentation.js";
|
|
3
|
-
import { storeCollectionSaving } from "../_shared/himi-store-collection.js";
|
|
4
|
-
import { defineBackendCollection } from "../_shared/collection-service.js";
|
|
5
|
-
import { getCurrentCart } from "@himalaya/wix-ecom";
|
|
6
|
-
import { getAll, upsert, type DbEntity } from "@himalaya/local-db";
|
|
7
|
-
import { productImage, productPrice, searchStoreProducts, type StoreProduct } from "../_shared/himi-store.js";
|
|
8
|
-
import { cleanTitle, initials } from "../_shared/himi-text.js";
|
|
9
|
-
import { copyParam } from "../_shared/himi-copy.js";
|
|
10
|
-
|
|
11
|
-
// Offline-first (data-offline): the product catalog is public read-through — paint
|
|
12
|
-
// the last-synced rows from the on-device store first (instant, works offline),
|
|
13
|
-
// then revalidate from Wix and refresh the cache. DEGRADES GRACEFULLY: every
|
|
14
|
-
// local-db call is wrapped so a bundle that ships in an app which hasn't granted
|
|
15
|
-
// `local_database` (+ declared the `products` entity in its dataSchema) just falls
|
|
16
|
-
// back to today's online-only path — the capability fails closed per-entity, so the
|
|
17
|
-
// try/catch is the seam that keeps this shared bundle safe across every consuming app.
|
|
18
|
-
const ENTITY = "products";
|
|
19
|
-
async function readCachedRows(): Promise<Row[]> {
|
|
20
|
-
try { return (await getAll(ENTITY)) as unknown as Row[]; } catch { return []; }
|
|
21
|
-
}
|
|
22
|
-
async function writeCachedRows(rows: Row[]): Promise<void> {
|
|
23
|
-
try { for (const r of rows) await upsert(ENTITY, r as unknown as DbEntity); } catch { /* capability not granted → online-only */ }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** Live cart size (sum of line-item quantities) for the top-bar cart badge.
|
|
27
|
-
* Best-effort: a cart error just yields 0 (badge hidden), never fails the screen. */
|
|
28
|
-
async function cartItemCount(): Promise<number> {
|
|
29
|
-
try {
|
|
30
|
-
const cart = await getCurrentCart();
|
|
31
|
-
return (cart.lineItems ?? []).reduce((n, li) => n + (li.quantity ?? 1), 0);
|
|
32
|
-
} catch {
|
|
33
|
-
return 0;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface Row {
|
|
38
|
-
id: string;
|
|
39
|
-
name: string;
|
|
40
|
-
subtitle: string;
|
|
41
|
-
commercePrice: string;
|
|
42
|
-
wasPrice: string;
|
|
43
|
-
image: string;
|
|
44
|
-
initial: string;
|
|
45
|
-
hasImage: boolean;
|
|
46
|
-
noImage: boolean;
|
|
47
|
-
detailScreenId: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface S {
|
|
51
|
-
status: "loading" | "ready" | "error";
|
|
52
|
-
total?: number;
|
|
53
|
-
complete?: boolean;
|
|
54
|
-
pageError?: boolean;
|
|
55
|
-
loadingMore?: boolean;
|
|
56
|
-
fromCache?: boolean;
|
|
57
|
-
query: string;
|
|
58
|
-
all: Row[];
|
|
59
|
-
collection?: { items: Array<Row & { collectionSubtitle: string }> };
|
|
60
|
-
results: Row[];
|
|
61
|
-
/** RFC 0008 cursor paging — non-empty while more pages exist ("" ⇒ exhausted). */
|
|
62
|
-
nextCursor: string;
|
|
63
|
-
emptyText: string;
|
|
64
|
-
countLabel: string;
|
|
65
|
-
cartCount: number;
|
|
66
|
-
isLoading: boolean;
|
|
67
|
-
notLoading: boolean;
|
|
68
|
-
isErrored: boolean;
|
|
69
|
-
notErrored: boolean;
|
|
70
|
-
noEmpty: boolean;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function row(p: StoreProduct): Row {
|
|
74
|
-
const name = cleanTitle(p.name) || "Product";
|
|
75
|
-
const image = productImage(p);
|
|
76
|
-
const id = p.id ?? "";
|
|
77
|
-
const priceData = p.priceData as { formatted?: { discountedPrice?: string; price?: string }; discountedPrice?: string; salePrice?: string } | undefined;
|
|
78
|
-
const regular = productPrice(p);
|
|
79
|
-
const discounted = priceData?.formatted?.discountedPrice ?? priceData?.discountedPrice ?? priceData?.salePrice;
|
|
80
|
-
const salePrice = discounted && discounted !== (priceData?.formatted?.price ?? regular)
|
|
81
|
-
? productPrice({ ...p, priceData: { ...priceData, formatted: { ...priceData?.formatted, price: discounted } } } as StoreProduct)
|
|
82
|
-
: "";
|
|
83
|
-
const originalV3 = p.compareAtPriceRange?.minValue
|
|
84
|
-
? productPrice({ ...p, price: undefined, priceData: undefined, actualPriceRange: p.compareAtPriceRange })
|
|
85
|
-
: "";
|
|
86
|
-
const wasPrice = salePrice ? regular : originalV3 && originalV3 !== regular ? originalV3 : "";
|
|
87
|
-
return {
|
|
88
|
-
id,
|
|
89
|
-
name,
|
|
90
|
-
subtitle: salePrice || regular,
|
|
91
|
-
commercePrice: salePrice || regular,
|
|
92
|
-
wasPrice,
|
|
93
|
-
image,
|
|
94
|
-
initial: initials(name),
|
|
95
|
-
hasImage: !!image,
|
|
96
|
-
noImage: !image,
|
|
97
|
-
// WS-C — canonical param-less detail screenId; the descriptor row binds the
|
|
98
|
-
// separate `id` field as a first-class nav param (no more baked suffixes).
|
|
99
|
-
detailScreenId: "himi_product",
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function derive(s: S): S {
|
|
104
|
-
const count = s.results.length;
|
|
105
|
-
return {
|
|
106
|
-
...s,
|
|
107
|
-
isLoading: s.status === "loading",
|
|
108
|
-
notLoading: s.status !== "loading",
|
|
109
|
-
isErrored: s.status === "error",
|
|
110
|
-
notErrored: s.status !== "error",
|
|
111
|
-
noEmpty: s.status === "loading" || s.status === "error" || count > 0,
|
|
112
|
-
countLabel: count ? `${count} product${count === 1 ? "" : "s"} from Wix Stores` : "",
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
let cacheWrite = Promise.resolve();
|
|
117
|
-
const svc = defineBackendCollection("himiStoreShop", {
|
|
118
|
-
register: withIapPresentation<S>({
|
|
119
|
-
prefix: "shop", transientKeys: ["all", "results", "collection"], contentActions: ["collectionDataChanged"],
|
|
120
|
-
items: state => state.results.map(row => ({ reference: { kind: "storesProduct", id: row.id }, commerceDisplayPrice: row.commercePrice ?? row.subtitle })),
|
|
121
|
-
present: (state, price) => ({ ...state, results: state.results.map(row => ({ ...row, subtitle: price(row.id) })), all: state.all.map(row => ({ ...row, subtitle: price(row.id) })),
|
|
122
|
-
...(state.collection ? { collection: { ...state.collection, items: state.collection.items.map(row => ({ ...row, collectionSubtitle: price(row.id) })) } } : {}) }),
|
|
123
|
-
}),
|
|
124
|
-
state: derive({ status: "loading", query: "", all: [], results: [], nextCursor: "",
|
|
125
|
-
emptyText: "", countLabel: "", cartCount: 0, isLoading: true, notLoading: false,
|
|
126
|
-
isErrored: false, notErrored: true, noEmpty: true }),
|
|
127
|
-
saving: storeCollectionSaving,
|
|
128
|
-
source: {
|
|
129
|
-
id: (item: Row) => item.id,
|
|
130
|
-
async cached(criteria) {
|
|
131
|
-
if (criteria.query || criteria.filter !== "all" || criteria.sort !== "default") return undefined;
|
|
132
|
-
const items = (await readCachedRows()).filter(item => !!item.id);
|
|
133
|
-
return items.length ? { items, nextCursor: "", complete: false, metadata: { fromCache: true } } : undefined;
|
|
134
|
-
},
|
|
135
|
-
async fetch(criteria, cursor) {
|
|
136
|
-
try {
|
|
137
|
-
const response = await searchStoreProducts({ query: criteria.query, sort: criteria.sort,
|
|
138
|
-
...(criteria.filter === "saved" ? { ids: criteria.savedIds } : {}), limit: 50, cursor });
|
|
139
|
-
const items = (response.products ?? []).map(row);
|
|
140
|
-
return { items, nextCursor: response.pagingMetadata?.cursors?.next ?? "",
|
|
141
|
-
total: response.pagingMetadata?.total,
|
|
142
|
-
metadata: cursor ? undefined : { cartCount: await cartItemCount(), fromCache: false } };
|
|
143
|
-
} catch (error) {
|
|
144
|
-
// A cached subset is never passed off as a backend search result.
|
|
145
|
-
const status = /HTTP (\d{3})/.exec(String(error))?.[1];
|
|
146
|
-
if (status !== "401" && status !== "403" && !cursor && !criteria.query && criteria.filter === "all" && criteria.sort === "default") {
|
|
147
|
-
const items = (await readCachedRows()).filter(item => !!item.id);
|
|
148
|
-
if (items.length) return { items, nextCursor: "", complete: false,
|
|
149
|
-
metadata: { fromCache: true } };
|
|
150
|
-
}
|
|
151
|
-
throw error;
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
apply(state, page) {
|
|
156
|
-
if (page.status === "ready" && !page.loadingMore && !page.pageError && !page.metadata?.fromCache) {
|
|
157
|
-
cacheWrite = cacheWrite.then(() => writeCachedRows(page.items));
|
|
158
|
-
}
|
|
159
|
-
const status = page.status === "idle" ? "loading" : page.status;
|
|
160
|
-
return derive({ ...state, ...page.metadata, status, query: page.criteria.query,
|
|
161
|
-
all: page.items, results: page.items, nextCursor: page.nextCursor,
|
|
162
|
-
total: page.total, complete: page.complete, pageError: page.pageError,
|
|
163
|
-
loadingMore: page.loadingMore, fromCache: page.metadata?.fromCache === true,
|
|
164
|
-
emptyText: status === "error" ? copyParam("shop.errorText", "Products could not load.")
|
|
165
|
-
: page.criteria.query || page.criteria.filter !== "all"
|
|
166
|
-
? copyParam("shop.noMatchText", "No products match this view.")
|
|
167
|
-
: copyParam("shop.emptyText", "No products are published yet."),
|
|
168
|
-
});
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
svc.load();
|
|
173
|
-
export { svc };
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { withCollectionSaving, type CollectionSaving } from './collection-service.js';
|
|
2
|
-
import { readSavedProducts, writeSavedProducts } from './himi-store-saved.js';
|
|
3
|
-
|
|
4
|
-
export const storeCollectionSaving: CollectionSaving = {
|
|
5
|
-
read: () => readSavedProducts().map(row => row.id),
|
|
6
|
-
write(ids, rows) {
|
|
7
|
-
const products = new Map(readSavedProducts().map(row => [row.id, row]));
|
|
8
|
-
for (const row of rows) {
|
|
9
|
-
if (typeof row.id !== 'string' || typeof row.name !== 'string') continue;
|
|
10
|
-
const previous = products.get(row.id);
|
|
11
|
-
const freshPrice = typeof row.commercePrice === 'string' ? row.commercePrice : typeof row.subtitle === 'string' ? row.subtitle : '';
|
|
12
|
-
const wasPrice = typeof row.wasPrice === 'string' ? row.wasPrice || undefined : previous?.price === freshPrice ? previous?.wasPrice : undefined;
|
|
13
|
-
products.set(row.id, { ...previous, wasPrice, id: row.id, name: row.name,
|
|
14
|
-
price: freshPrice || previous?.price || '',
|
|
15
|
-
image: typeof row.image === 'string' && row.image ? row.image : previous?.image ?? '' });
|
|
16
|
-
}
|
|
17
|
-
const selected = ids.map(id => products.get(id));
|
|
18
|
-
if (selected.some(row => !row)) throw new Error('Saved product metadata is unavailable');
|
|
19
|
-
writeSavedProducts(selected.filter((row): row is NonNullable<typeof row> => !!row));
|
|
20
|
-
},
|
|
21
|
-
};
|
|
22
|
-
export const defineService = withCollectionSaving(storeCollectionSaving);
|