@wix/himalaya-cli 0.1497.0 → 0.1499.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.
Files changed (146) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-collections/src/display.d.ts +1 -1
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-collections/src/index.d.ts +2 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-v1/src/display.d.ts +4 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-stores-collections.mjs +2 -2
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-stores.mjs +3 -2
  6. package/dist/cli.mjs +30 -30
  7. package/dist/preview-assets.generated.bin +0 -0
  8. package/guides/sdk-reference.json +8 -2
  9. package/guides/trim-a-template.md +1 -1
  10. package/package.json +2 -2
  11. package/templates/beauty-salon/dev/index.ts +2465 -2035
  12. package/templates/beauty-salon/tier5-src/_shared/himi-store-categories.ts +102 -0
  13. package/templates/beauty-salon/tier5-src/_shared/himi-store-facets.ts +478 -0
  14. package/templates/beauty-salon/tier5-src/_shared/iap-presentation.ts +158 -15
  15. package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +42 -2
  16. package/templates/beauty-salon/tier5-src/himi-branded-lite-shop/index.ts +691 -0
  17. package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +2 -1
  18. package/templates/beauty-salon/tier5-src/himi-store-saved/index.ts +1 -0
  19. package/templates/beauty-salon-owner/dev/index.ts +571 -0
  20. package/templates/beauty-salon-owner/himalaya.content.json +6 -0
  21. package/templates/beauty-salon-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  22. package/templates/beauty-salon-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  23. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  24. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  25. package/templates/branded-lite/dev/index.ts +2989 -2197
  26. package/templates/branded-lite/tier5-src/_shared/business-chat-presenter.ts +46 -8
  27. package/templates/branded-lite/tier5-src/_shared/himi-store-categories.ts +102 -0
  28. package/templates/branded-lite/tier5-src/_shared/himi-store-facets.ts +478 -0
  29. package/templates/branded-lite/tier5-src/_shared/iap-presentation.ts +158 -15
  30. package/templates/branded-lite/tier5-src/business-chat/index.ts +3 -2
  31. package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +42 -2
  32. package/templates/branded-lite/tier5-src/himi-branded-lite-shop/index.ts +691 -0
  33. package/templates/branded-lite/tier5-src/himi-store-product/index.ts +2 -1
  34. package/templates/branded-lite/tier5-src/himi-store-saved/index.ts +1 -0
  35. package/templates/community-club/tier5-src/_shared/himi-store-categories.ts +102 -0
  36. package/templates/community-club/tier5-src/_shared/iap-presentation.ts +158 -15
  37. package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +42 -2
  38. package/templates/community-club-owner/dev/index.ts +571 -0
  39. package/templates/community-club-owner/himalaya.content.json +6 -0
  40. package/templates/community-club-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  41. package/templates/community-club-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  42. package/templates/community-club-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  43. package/templates/community-club-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  44. package/templates/events-meetups/tier5-src/_shared/himi-store-categories.ts +102 -0
  45. package/templates/events-meetups/tier5-src/_shared/iap-presentation.ts +158 -15
  46. package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +42 -2
  47. package/templates/events-meetups-owner/dev/index.ts +571 -0
  48. package/templates/events-meetups-owner/himalaya.content.json +6 -0
  49. package/templates/events-meetups-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  50. package/templates/events-meetups-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  51. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  52. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  53. package/templates/gym-fitness/dev/index.ts +2600 -2170
  54. package/templates/gym-fitness/tier5-src/_shared/himi-store-categories.ts +102 -0
  55. package/templates/gym-fitness/tier5-src/_shared/himi-store-facets.ts +478 -0
  56. package/templates/gym-fitness/tier5-src/_shared/iap-presentation.ts +158 -15
  57. package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +42 -2
  58. package/templates/gym-fitness/tier5-src/himi-branded-lite-shop/index.ts +691 -0
  59. package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +2 -1
  60. package/templates/gym-fitness/tier5-src/himi-store-saved/index.ts +1 -0
  61. package/templates/gym-fitness-owner/dev/index.ts +571 -0
  62. package/templates/gym-fitness-owner/himalaya.content.json +6 -0
  63. package/templates/gym-fitness-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  64. package/templates/gym-fitness-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  65. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  66. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  67. package/templates/nonprofit-cause/tier5-src/_shared/himi-store-categories.ts +102 -0
  68. package/templates/nonprofit-cause/tier5-src/_shared/iap-presentation.ts +158 -15
  69. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +42 -2
  70. package/templates/nonprofit-cause-owner/dev/index.ts +571 -0
  71. package/templates/nonprofit-cause-owner/himalaya.content.json +6 -0
  72. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  73. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  74. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  75. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  76. package/templates/owner-lite/dev/index.ts +571 -0
  77. package/templates/owner-lite/himalaya.content.json +6 -0
  78. package/templates/owner-lite/tier5-src/_shared/owner-today-rollup.ts +307 -0
  79. package/templates/owner-lite/tier5-src/_shared/owner-widget-reads.ts +23 -0
  80. package/templates/owner-lite/tier5-src/himi-owner-lite-today/index.ts +8 -20
  81. package/templates/owner-lite/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  82. package/templates/podcast-creator/tier5-src/_shared/himi-store-categories.ts +102 -0
  83. package/templates/podcast-creator/tier5-src/_shared/iap-presentation.ts +158 -15
  84. package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +42 -2
  85. package/templates/podcast-creator-owner/dev/index.ts +571 -0
  86. package/templates/podcast-creator-owner/himalaya.content.json +6 -0
  87. package/templates/podcast-creator-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  88. package/templates/podcast-creator-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  89. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  90. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  91. package/templates/portfolio-creative/dev/index.ts +2470 -2040
  92. package/templates/portfolio-creative/tier5-src/_shared/himi-store-categories.ts +102 -0
  93. package/templates/portfolio-creative/tier5-src/_shared/himi-store-facets.ts +478 -0
  94. package/templates/portfolio-creative/tier5-src/_shared/iap-presentation.ts +158 -15
  95. package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +42 -2
  96. package/templates/portfolio-creative/tier5-src/himi-branded-lite-shop/index.ts +691 -0
  97. package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +2 -1
  98. package/templates/portfolio-creative/tier5-src/himi-store-saved/index.ts +1 -0
  99. package/templates/portfolio-creative-owner/dev/index.ts +571 -0
  100. package/templates/portfolio-creative-owner/himalaya.content.json +6 -0
  101. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  102. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  103. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  104. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  105. package/templates/restaurant/tier5-src/_shared/himi-store-categories.ts +102 -0
  106. package/templates/restaurant/tier5-src/_shared/iap-presentation.ts +158 -15
  107. package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +42 -2
  108. package/templates/restaurant-owner/dev/index.ts +571 -0
  109. package/templates/restaurant-owner/himalaya.content.json +6 -0
  110. package/templates/restaurant-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  111. package/templates/restaurant-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  112. package/templates/restaurant-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  113. package/templates/restaurant-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  114. package/templates/retail-store/dev/index.ts +2583 -2137
  115. package/templates/retail-store/tier5-src/_shared/himi-store-categories.ts +102 -0
  116. package/templates/retail-store/tier5-src/_shared/himi-store-facets.ts +478 -0
  117. package/templates/retail-store/tier5-src/_shared/iap-presentation.ts +158 -15
  118. package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +42 -2
  119. package/templates/retail-store/tier5-src/himi-branded-lite-shop/index.ts +691 -0
  120. package/templates/retail-store/tier5-src/himi-store-product/index.ts +2 -1
  121. package/templates/retail-store/tier5-src/himi-store-saved/index.ts +1 -0
  122. package/templates/retail-store-owner/dev/index.ts +571 -0
  123. package/templates/retail-store-owner/himalaya.content.json +6 -0
  124. package/templates/retail-store-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  125. package/templates/retail-store-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  126. package/templates/retail-store-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  127. package/templates/retail-store-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  128. package/templates/yoga-studio/tier5-src/_shared/himi-store-categories.ts +102 -0
  129. package/templates/yoga-studio/tier5-src/_shared/iap-presentation.ts +158 -15
  130. package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +42 -2
  131. package/templates/yoga-studio-owner/dev/index.ts +571 -0
  132. package/templates/yoga-studio-owner/himalaya.content.json +6 -0
  133. package/templates/yoga-studio-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
  134. package/templates/yoga-studio-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
  135. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
  136. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
  137. package/templates/beauty-salon/tier5-src/_shared/himi-store-collection.ts +0 -22
  138. package/templates/beauty-salon/tier5-src/himi-store-shop/index.ts +0 -173
  139. package/templates/branded-lite/tier5-src/_shared/himi-store-collection.ts +0 -22
  140. package/templates/branded-lite/tier5-src/himi-store-shop/index.ts +0 -173
  141. package/templates/gym-fitness/tier5-src/_shared/himi-store-collection.ts +0 -22
  142. package/templates/gym-fitness/tier5-src/himi-store-shop/index.ts +0 -173
  143. package/templates/portfolio-creative/tier5-src/_shared/himi-store-collection.ts +0 -22
  144. package/templates/portfolio-creative/tier5-src/himi-store-shop/index.ts +0 -173
  145. package/templates/retail-store/tier5-src/_shared/himi-store-collection.ts +0 -22
  146. 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);