@wix/himalaya-cli 0.1494.0 → 0.1495.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 +3 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/live-activity/src/index.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +2 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/ecom/src/extensions.d.ts +16 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/stores-v1/src/display.d.ts +24 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/live-activity.mjs +3 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-ecom.mjs +14 -0
- package/dist/cli.mjs +3584 -2540
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/himi-json-output.md +46 -4
- package/guides/iap-products.md +68 -9
- package/guides/one-app-to-himi-app.md +80 -5
- package/guides/sdk-reference.json +11 -256
- package/guides/widgets.md +5 -3
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +2590 -1034
- package/templates/beauty-salon/himalaya.content.json +1 -1
- package/templates/beauty-salon/tier5-src/_shared/himi-store-collection.ts +6 -3
- package/templates/beauty-salon/tier5-src/_shared/himi-store-saved.ts +7 -1
- package/templates/beauty-salon/tier5-src/himi-branded-lite-cart/index.ts +256 -19
- package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +196 -34
- package/templates/beauty-salon/tier5-src/himi-store-saved/index.ts +21 -5
- package/templates/beauty-salon/tier5-src/himi-store-shop/index.ts +14 -2
- package/templates/beauty-salon/tokens.json +15 -0
- package/templates/branded-lite/dev/index.ts +2696 -1140
- package/templates/branded-lite/himalaya.content.json +1 -1
- package/templates/branded-lite/tier5-src/_shared/himi-store-collection.ts +6 -3
- package/templates/branded-lite/tier5-src/_shared/himi-store-saved.ts +7 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-cart/index.ts +256 -19
- package/templates/branded-lite/tier5-src/himi-store-product/index.ts +196 -34
- package/templates/branded-lite/tier5-src/himi-store-saved/index.ts +21 -5
- package/templates/branded-lite/tier5-src/himi-store-shop/index.ts +14 -2
- package/templates/branded-lite/tokens.json +8 -0
- package/templates/community-club/dev/index.ts +1 -1
- package/templates/community-club/tier5-src/_shared/himi-store-saved.ts +7 -1
- package/templates/community-club/tokens.json +15 -0
- package/templates/events-meetups/dev/index.ts +1 -1
- package/templates/events-meetups/tier5-src/_shared/himi-store-saved.ts +7 -1
- package/templates/events-meetups/tokens.json +15 -0
- package/templates/gym-fitness/dev/index.ts +2695 -1139
- package/templates/gym-fitness/himalaya.content.json +1 -1
- package/templates/gym-fitness/tier5-src/_shared/himi-store-collection.ts +6 -3
- package/templates/gym-fitness/tier5-src/_shared/himi-store-saved.ts +7 -1
- package/templates/gym-fitness/tier5-src/himi-branded-lite-cart/index.ts +256 -19
- package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +196 -34
- package/templates/gym-fitness/tier5-src/himi-store-saved/index.ts +21 -5
- package/templates/gym-fitness/tier5-src/himi-store-shop/index.ts +14 -2
- package/templates/gym-fitness/tokens.json +15 -0
- package/templates/nonprofit-cause/dev/index.ts +1 -1
- package/templates/nonprofit-cause/tier5-src/_shared/himi-store-saved.ts +7 -1
- package/templates/nonprofit-cause/tokens.json +15 -0
- package/templates/podcast-creator/dev/index.ts +1 -1
- package/templates/podcast-creator/tier5-src/_shared/himi-store-saved.ts +7 -1
- package/templates/podcast-creator/tokens.json +15 -0
- package/templates/portfolio-creative/dev/index.ts +2569 -1013
- package/templates/portfolio-creative/himalaya.content.json +1 -1
- package/templates/portfolio-creative/tier5-src/_shared/himi-store-collection.ts +6 -3
- package/templates/portfolio-creative/tier5-src/_shared/himi-store-saved.ts +7 -1
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-cart/index.ts +256 -19
- package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +196 -34
- package/templates/portfolio-creative/tier5-src/himi-store-saved/index.ts +21 -5
- package/templates/portfolio-creative/tier5-src/himi-store-shop/index.ts +14 -2
- package/templates/portfolio-creative/tokens.json +15 -0
- package/templates/restaurant/dev/index.ts +1 -1
- package/templates/restaurant/tier5-src/_shared/himi-store-saved.ts +7 -1
- package/templates/restaurant/tokens.json +15 -0
- package/templates/retail-store/dev/index.ts +2541 -985
- package/templates/retail-store/himalaya.content.json +1 -1
- package/templates/retail-store/tier5-src/_shared/himi-store-collection.ts +6 -3
- package/templates/retail-store/tier5-src/_shared/himi-store-saved.ts +7 -1
- package/templates/retail-store/tier5-src/himi-branded-lite-cart/index.ts +256 -19
- package/templates/retail-store/tier5-src/himi-store-product/index.ts +196 -34
- package/templates/retail-store/tier5-src/himi-store-saved/index.ts +21 -5
- package/templates/retail-store/tier5-src/himi-store-shop/index.ts +14 -2
- package/templates/retail-store/tokens.json +15 -0
- package/templates/yoga-studio/dev/index.ts +1 -1
- package/templates/yoga-studio/tier5-src/_shared/himi-store-saved.ts +7 -1
- package/templates/yoga-studio/tokens.json +15 -0
|
@@ -48,6 +48,8 @@ interface GalleryItem {
|
|
|
48
48
|
interface OptionChip {
|
|
49
49
|
id: string;
|
|
50
50
|
label: string;
|
|
51
|
+
swatch?: string;
|
|
52
|
+
disabled?: boolean;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
// ---------------------------------------------------------------------------
|
|
@@ -72,6 +74,20 @@ const OPTION_SLOTS = 3;
|
|
|
72
74
|
* payload is resolved from `variantsInfo`, which no renderer needs to see. */
|
|
73
75
|
let lastProduct: StoreProduct | undefined;
|
|
74
76
|
|
|
77
|
+
/** Match only merchant-authored product sections with a clear policy title. */
|
|
78
|
+
function productPolicyBody(product: StoreProduct, kind: "returns" | "shipping"): string {
|
|
79
|
+
const sections = [...(product.infoSections ?? []), ...(product.additionalInfoSections ?? [])];
|
|
80
|
+
const section = sections.find((item) => {
|
|
81
|
+
const name = `${item.title ?? ""} ${"uniqueName" in item ? item.uniqueName ?? "" : ""}`;
|
|
82
|
+
return kind === "returns" ? /return|refund/i.test(name) : /shipping|delivery/i.test(name);
|
|
83
|
+
});
|
|
84
|
+
if (!section) return "";
|
|
85
|
+
const html = "plainDescription" in section ? section.plainDescription : undefined;
|
|
86
|
+
if (html) return plainText(html.replace(/ /gi, " "));
|
|
87
|
+
if (typeof section.description === "string") return plainText(section.description.replace(/ /gi, " "));
|
|
88
|
+
return richBody(section.description).bodyText;
|
|
89
|
+
}
|
|
90
|
+
|
|
75
91
|
/** The product id `lastProduct` was read for. A pooled screen re-appearing calls
|
|
76
92
|
* `refreshServices()` → `load()`, so this is what tells a genuine navigation to a
|
|
77
93
|
* DIFFERENT product (drop the picks) from the same product coming back (keep them). */
|
|
@@ -79,23 +95,31 @@ let lastLoadedId: string | undefined;
|
|
|
79
95
|
let productLoadSequence = 0;
|
|
80
96
|
|
|
81
97
|
interface OptionSlots {
|
|
82
|
-
opt1Label: string; opt1Chips: OptionChip[]; opt1Selected: string[]; noOpt1: boolean;
|
|
83
|
-
opt2Label: string; opt2Chips: OptionChip[]; opt2Selected: string[]; noOpt2: boolean;
|
|
84
|
-
opt3Label: string; opt3Chips: OptionChip[]; opt3Selected: string[]; noOpt3: boolean;
|
|
98
|
+
opt1Label: string; opt1Chips: OptionChip[]; opt1Selected: string[]; opt1SelectedId: string; opt1Placeholder: string; noOpt1: boolean;
|
|
99
|
+
opt2Label: string; opt2Chips: OptionChip[]; opt2Selected: string[]; opt2SelectedId: string; opt2Placeholder: string; noOpt2: boolean;
|
|
100
|
+
opt3Label: string; opt3Chips: OptionChip[]; opt3Selected: string[]; opt3SelectedId: string; opt3Placeholder: string; noOpt3: boolean;
|
|
101
|
+
opt1Error: string; noOpt1Error: boolean; hasOpt1Error: boolean; opt2Error: string; noOpt2Error: boolean; hasOpt2Error: boolean; opt3Error: string; noOpt3Error: boolean; hasOpt3Error: boolean;
|
|
85
102
|
}
|
|
86
103
|
|
|
87
|
-
function slotsOf(groups: StoreOption[], selection: StoreOptionSelection): OptionSlots {
|
|
104
|
+
function slotsOf(groups: StoreOption[], selection: StoreOptionSelection, showErrors: boolean): OptionSlots {
|
|
88
105
|
const out = {} as Record<string, unknown>;
|
|
89
106
|
for (let i = 0; i < OPTION_SLOTS; i++) {
|
|
90
107
|
const g = groups[i];
|
|
91
108
|
const n = i + 1;
|
|
92
109
|
const chosen = g ? selection[g.id] ?? "" : "";
|
|
93
|
-
|
|
110
|
+
const displayName = g?.name ? g.name.charAt(0).toUpperCase() + g.name.slice(1).toLowerCase() : "";
|
|
111
|
+
out[`opt${n}Label`] = displayName ? copyTemplate("product.optionLabel", "{option}:", { option: displayName }) : "";
|
|
94
112
|
// Out-of-stock choices stay selectable but say so — hiding them makes a product
|
|
95
113
|
// look like it has fewer colours than it does, and the stock flag is per-choice
|
|
96
114
|
// across variants, not a promise about the combination.
|
|
97
|
-
out[`opt${n}Chips`] = (g?.choices ?? []).map((c) => ({ id: c.id, label: c.
|
|
115
|
+
out[`opt${n}Chips`] = (g?.choices ?? []).map((c) => ({ id: c.id, label: c.label, ...(g.kind === "color" && c.colorCode ? { swatch: c.colorCode } : {}), disabled: c.inStock === false }));
|
|
98
116
|
out[`opt${n}Selected`] = chosen ? [chosen] : [];
|
|
117
|
+
out[`opt${n}SelectedId`] = chosen;
|
|
118
|
+
out[`opt${n}Placeholder`] = g?.choices.find(c => c.id === chosen)?.label ?? copyTemplate("product.optionPlaceholder", "Choose {option}", { option: displayName || "option" });
|
|
119
|
+
const error = showErrors && g && !chosen ? copyTemplate("product.optionRequired", "Choose a {option}", { option: g.name.toLowerCase() }) : "";
|
|
120
|
+
out[`opt${n}Error`] = error;
|
|
121
|
+
out[`noOpt${n}Error`] = !error;
|
|
122
|
+
out[`hasOpt${n}Error`] = !!error;
|
|
99
123
|
out[`noOpt${n}`] = !g;
|
|
100
124
|
}
|
|
101
125
|
return out as unknown as OptionSlots;
|
|
@@ -135,10 +159,76 @@ function productBody(p: StoreProduct): { description: string; descriptionJson: s
|
|
|
135
159
|
return { description: html || bodyText, descriptionJson: bodyJson };
|
|
136
160
|
}
|
|
137
161
|
|
|
162
|
+
function productSale(p: StoreProduct): { price: string; wasPrice: string } {
|
|
163
|
+
const raw = p as unknown as { priceData?: Record<string, unknown> };
|
|
164
|
+
const pd = raw.priceData ?? {};
|
|
165
|
+
const formatted = (pd.formatted ?? {}) as Record<string, unknown>;
|
|
166
|
+
const regular = productPrice(p);
|
|
167
|
+
if (p.compareAtPriceRange?.minValue) {
|
|
168
|
+
const original = productPrice({ ...p, price: undefined, priceData: undefined, actualPriceRange: p.compareAtPriceRange });
|
|
169
|
+
if (original && original !== regular) return { price: regular, wasPrice: original };
|
|
170
|
+
}
|
|
171
|
+
const rawRegular = typeof formatted.price === "string" ? formatted.price : regular;
|
|
172
|
+
const saleRaw = formatted.discountedPrice ?? pd.discountedPrice ?? pd.salePrice;
|
|
173
|
+
const sale = typeof saleRaw === "string" ? saleRaw : "";
|
|
174
|
+
const saleDisplay = sale ? productPrice({ ...p, priceData: { ...pd, formatted: { ...formatted, price: sale } } } as StoreProduct) : "";
|
|
175
|
+
return sale && sale !== rawRegular ? { price: saleDisplay, wasPrice: regular } : { price: regular, wasPrice: "" };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The optimistic cart has no Wix-calculated line price yet. Multiply the formatted
|
|
179
|
+
* unit amount while preserving its symbol, separators and decimal precision. */
|
|
180
|
+
function optimisticLineTotal(unitPrice: string, quantity: number): string {
|
|
181
|
+
if (quantity <= 1) return unitPrice;
|
|
182
|
+
const match = /\d(?:[\d.,]|[ \u00a0\u202f](?=\d))*/u.exec(unitPrice);
|
|
183
|
+
if (!match) return "";
|
|
184
|
+
const raw = match[0];
|
|
185
|
+
const compact = raw.replace(/[ \u00a0\u202f]/g, "");
|
|
186
|
+
const last = Math.max(compact.lastIndexOf("."), compact.lastIndexOf(","));
|
|
187
|
+
const fractionalDigits = last >= 0 ? compact.length - last - 1 : 0;
|
|
188
|
+
const decimal = fractionalDigits > 0 && fractionalDigits <= 2 ? compact[last] : "";
|
|
189
|
+
const punctuationGrouping = decimal === "." ? "," : decimal === "," ? "." : compact.includes(",") ? "," : compact.includes(".") ? "." : "";
|
|
190
|
+
const grouping = raw.match(/[ \u00a0\u202f](?=\d)/u)?.[0] ?? punctuationGrouping;
|
|
191
|
+
const amount = Number(compact.replaceAll(punctuationGrouping || "\u0000", "").replace(decimal || "\u0000", "."));
|
|
192
|
+
if (!Number.isFinite(amount)) return "";
|
|
193
|
+
const fixed = (amount * quantity).toFixed(decimal ? fractionalDigits : 0);
|
|
194
|
+
const [whole, fraction] = fixed.split(".");
|
|
195
|
+
const grouped = grouping ? whole.replace(/\B(?=(\d{3})+(?!\d))/g, grouping) : whole;
|
|
196
|
+
return unitPrice.replace(raw, `${grouped}${decimal ? decimal + fraction : ""}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function skuOf(p: StoreProduct): string {
|
|
200
|
+
const raw = p as unknown as { sku?: unknown; variantsInfo?: { variants?: Array<{ sku?: unknown }> } };
|
|
201
|
+
if (typeof raw.sku === "string" && raw.sku.trim()) return raw.sku.trim();
|
|
202
|
+
const sku = raw.variantsInfo?.variants?.find(v => typeof v.sku === "string" && v.sku.trim())?.sku;
|
|
203
|
+
return typeof sku === "string" ? sku.trim() : "";
|
|
204
|
+
}
|
|
205
|
+
|
|
138
206
|
interface S {
|
|
139
207
|
status: "loading" | "ready" | "error";
|
|
140
208
|
name: string;
|
|
141
209
|
price: string;
|
|
210
|
+
wasPrice: string;
|
|
211
|
+
notOnSale: boolean;
|
|
212
|
+
wasPriceEmpty: boolean;
|
|
213
|
+
noSku: boolean;
|
|
214
|
+
skuLabel: string;
|
|
215
|
+
returnPolicyBody: string;
|
|
216
|
+
shippingInfoBody: string;
|
|
217
|
+
noReturnPolicyBody: boolean;
|
|
218
|
+
noShippingInfoBody: boolean;
|
|
219
|
+
returnPolicyCollapsed: boolean;
|
|
220
|
+
shippingInfoCollapsed: boolean;
|
|
221
|
+
noSaveHint: boolean;
|
|
222
|
+
saveIcon: string;
|
|
223
|
+
optionsSummary: string;
|
|
224
|
+
quantityLabel: string;
|
|
225
|
+
quantityIsOne: boolean;
|
|
226
|
+
addedSheetPresented: boolean;
|
|
227
|
+
shareHint: string;
|
|
228
|
+
noShareHint: boolean;
|
|
229
|
+
noCartHint: boolean;
|
|
230
|
+
hideBar: boolean;
|
|
231
|
+
hideProduct: boolean;
|
|
142
232
|
commercePrice?: string;
|
|
143
233
|
/** The plain-text description: RichContent's fallback, and what shows with no document. */
|
|
144
234
|
description: string;
|
|
@@ -147,6 +237,8 @@ interface S {
|
|
|
147
237
|
descriptionJson: string;
|
|
148
238
|
/** descriptionJson !== "" — gates the plain text, because RichContent draws nothing for "". */
|
|
149
239
|
hasDescriptionJson: boolean;
|
|
240
|
+
returnPolicyExpanded: boolean;
|
|
241
|
+
shippingInfoExpanded: boolean;
|
|
150
242
|
image: string;
|
|
151
243
|
/** FULL media gallery (media.items[] with the main image first) — the detail
|
|
152
244
|
* screen renders a Carousel over it when 2+ images exist. */
|
|
@@ -194,14 +286,17 @@ interface S {
|
|
|
194
286
|
noOptionsOverflow: boolean;
|
|
195
287
|
/** Add to cart is blocked: this product has options and they are not all chosen. */
|
|
196
288
|
optionsIncomplete: boolean;
|
|
289
|
+
showOptionErrors: boolean;
|
|
290
|
+
addDisabled: boolean;
|
|
197
291
|
/** The product was served from the offline cache, which carries no option groups.
|
|
198
292
|
* "No groups" here means UNKNOWN, not "none" — so the CTA must stay blocked rather
|
|
199
293
|
* than offer a one-tap add for a product that may need a colour and a size. */
|
|
200
294
|
optionsUnknown: boolean;
|
|
201
295
|
optionsHint: string;
|
|
202
|
-
opt1Label: string; opt1Chips: OptionChip[]; opt1Selected: string[]; noOpt1: boolean;
|
|
203
|
-
opt2Label: string; opt2Chips: OptionChip[]; opt2Selected: string[]; noOpt2: boolean;
|
|
204
|
-
opt3Label: string; opt3Chips: OptionChip[]; opt3Selected: string[]; noOpt3: boolean;
|
|
296
|
+
opt1Label: string; opt1Chips: OptionChip[]; opt1Selected: string[]; opt1SelectedId: string; opt1Placeholder: string; noOpt1: boolean;
|
|
297
|
+
opt2Label: string; opt2Chips: OptionChip[]; opt2Selected: string[]; opt2SelectedId: string; opt2Placeholder: string; noOpt2: boolean;
|
|
298
|
+
opt3Label: string; opt3Chips: OptionChip[]; opt3Selected: string[]; opt3SelectedId: string; opt3Placeholder: string; noOpt3: boolean;
|
|
299
|
+
opt1Error: string; noOpt1Error: boolean; hasOpt1Error: boolean; opt2Error: string; noOpt2Error: boolean; hasOpt2Error: boolean; opt3Error: string; noOpt3Error: boolean; hasOpt3Error: boolean;
|
|
205
300
|
/** The App Store action (`commerce.purchaseFields`), in place of Add to cart while it shows. */
|
|
206
301
|
cartHidden: boolean;
|
|
207
302
|
buyHidden: boolean;
|
|
@@ -281,10 +376,27 @@ function derive(s: S): S {
|
|
|
281
376
|
const hasSingleImage = !!s.image && !hasGallery;
|
|
282
377
|
const groups = s.optionGroups;
|
|
283
378
|
const overflow = groups.length > OPTION_SLOTS;
|
|
379
|
+
const optionsIncomplete = s.optionsUnknown || !!outOfStockPick(groups, s.selection) || (groups.length > 0 && (overflow || !isSelectionComplete(groups, s.selection)));
|
|
284
380
|
return {
|
|
285
381
|
...s,
|
|
382
|
+
notOnSale: !s.wasPrice,
|
|
383
|
+
wasPriceEmpty: !!s.wasPrice,
|
|
384
|
+
noSku: !s.skuLabel,
|
|
385
|
+
noReturnPolicyBody: !s.returnPolicyBody,
|
|
386
|
+
noShippingInfoBody: !s.shippingInfoBody,
|
|
387
|
+
returnPolicyCollapsed: !s.returnPolicyExpanded,
|
|
388
|
+
shippingInfoCollapsed: !s.shippingInfoExpanded,
|
|
389
|
+
noSaveHint: !s.saveHint,
|
|
390
|
+
noShareHint: !s.shareHint,
|
|
391
|
+
noCartHint: !s.cartHint || s.added,
|
|
392
|
+
hideBar: s.status !== "ready" || !s.name || s.addedSheetPresented,
|
|
393
|
+
hideProduct: s.status !== "ready" || !s.name,
|
|
394
|
+
saveIcon: s.saved ? "heart_fill" : "heart",
|
|
395
|
+
optionsSummary: selectionSummary(s.optionGroups, s.selection),
|
|
396
|
+
quantityLabel: `Qty: ${s.quantity}`,
|
|
397
|
+
quantityIsOne: s.quantity <= 1,
|
|
286
398
|
hasDescriptionJson: s.descriptionJson !== "",
|
|
287
|
-
...slotsOf(groups, s.selection),
|
|
399
|
+
...slotsOf(groups, s.selection, s.showOptionErrors),
|
|
288
400
|
hasOptions: groups.length > 0,
|
|
289
401
|
noOptions: groups.length === 0,
|
|
290
402
|
optionsOverflow: overflow,
|
|
@@ -294,9 +406,10 @@ function derive(s: S): S {
|
|
|
294
406
|
// `optionsUnknown` blocks too: a cached product whose options we never read is
|
|
295
407
|
// indistinguishable from one with none, and adding it would post the variant-less
|
|
296
408
|
// line this whole feature exists to prevent.
|
|
297
|
-
optionsIncomplete
|
|
409
|
+
optionsIncomplete,
|
|
410
|
+
addDisabled: optionsIncomplete,
|
|
298
411
|
optionsHint: groups.length ? optionsHint(groups, s.selection) : "",
|
|
299
|
-
saveLabel: s.saved ? copyParam("
|
|
412
|
+
saveLabel: s.saved ? copyParam("savedProducts.removeLabel", "Remove from wishlist") : copyParam("savedProducts.addLabel", "Add to wishlist"),
|
|
300
413
|
notAdding: !s.adding,
|
|
301
414
|
notAdded: !s.added,
|
|
302
415
|
isLoading: s.status === "loading",
|
|
@@ -329,15 +442,16 @@ let checkoutEntered = false;
|
|
|
329
442
|
const defineService = withIapPresentation<S>({
|
|
330
443
|
adapters: SELLS_THROUGH_APPLE ? [storesProductOrders] : [],
|
|
331
444
|
onCreate: commerce => { productPricing = commerce; }, prefix: "product",
|
|
332
|
-
transientKeys: ["price", "optionsIncomplete", "cartHint", "cartHidden", "buyHidden", "buyLabel", "buyDisabled", "buyNote", "purchasedNote"], contentActions: ["load", "paintCached"],
|
|
445
|
+
transientKeys: ["price", "optionsIncomplete", "cartHint", "cartHidden", "buyHidden", "buyLabel", "buyDisabled", "buyNote", "purchasedNote", "addedSheetPresented", "shareHint"], contentActions: ["load", "paintCached"],
|
|
333
446
|
items: state => inputId() ? [{ reference: { kind: "storesProduct", id: inputId() }, commerceDisplayPrice: state.price }] : [],
|
|
334
447
|
present: (state, price, allowed) => {
|
|
335
448
|
const purchase = applePurchase(state);
|
|
336
449
|
const unavailable = iapPriceCopy("product").purchaseUnavailable;
|
|
337
450
|
const blocked = lastProduct?.productType?.toLowerCase() === "digital" || (!state.optionsUnknown && !allowed(inputId()));
|
|
451
|
+
const cartHint = !purchase.appleRoute && blocked ? unavailable : state.cartHint === unavailable ? "" : state.cartHint;
|
|
338
452
|
return {
|
|
339
|
-
...state, price: price(inputId()), optionsIncomplete: derive(state).optionsIncomplete || !allowed(inputId()) || lastProduct?.productType?.toLowerCase() === "digital",
|
|
340
|
-
cartHint: !
|
|
453
|
+
...state, price: price(inputId()), ...(purchase.appleRoute ? { wasPrice: "", wasPriceEmpty: false, notOnSale: true } : {}), optionsIncomplete: derive(state).optionsIncomplete || !allowed(inputId()) || lastProduct?.productType?.toLowerCase() === "digital",
|
|
454
|
+
cartHint, noCartHint: !cartHint || state.added, addDisabled: state.addDisabled || blocked,
|
|
341
455
|
cartHidden: purchase.appleRoute,
|
|
342
456
|
buyHidden: !purchase.appleRoute || purchase.accessReady, buyLabel: purchase.buyLabel, buyDisabled: purchase.buyDisabled, buyNote: purchase.buyNote,
|
|
343
457
|
purchasedNote: purchase.accessReady ? copyParam("product.purchasedText", "Purchased") : "",
|
|
@@ -367,6 +481,8 @@ function enterCheckout(): void {
|
|
|
367
481
|
const svc = defineService<S, {
|
|
368
482
|
load: (s: S, arg?: unknown) => Promise<S>;
|
|
369
483
|
setQuantity: (s: S, arg?: unknown) => S;
|
|
484
|
+
toggleReturnPolicy: (s: S) => S;
|
|
485
|
+
toggleShippingInfo: (s: S) => S;
|
|
370
486
|
selectOption: (s: S, arg?: unknown) => S;
|
|
371
487
|
addToCart: (s: S) => Promise<S>;
|
|
372
488
|
beginAdd: (s: S, arg?: unknown) => S;
|
|
@@ -374,6 +490,7 @@ const svc = defineService<S, {
|
|
|
374
490
|
setCartCount: (s: S, arg?: unknown) => S;
|
|
375
491
|
share: (s: S) => Promise<S>;
|
|
376
492
|
toggleSave: (s: S) => S;
|
|
493
|
+
closeAddedSheet: (s: S) => S;
|
|
377
494
|
/** [rico] #3372 — RichContent's on_link: `{ url }` of the tapped link. */
|
|
378
495
|
openLink: (s: S, arg?: unknown) => Promise<S>;
|
|
379
496
|
/** The App Store Buy: through the shared checkout, for the product on screen. */
|
|
@@ -383,9 +500,33 @@ const svc = defineService<S, {
|
|
|
383
500
|
status: "loading",
|
|
384
501
|
name: "",
|
|
385
502
|
price: "",
|
|
503
|
+
wasPrice: "",
|
|
504
|
+
notOnSale: true,
|
|
505
|
+
wasPriceEmpty: false,
|
|
506
|
+
skuLabel: "",
|
|
507
|
+
noSku: true,
|
|
508
|
+
returnPolicyBody: "",
|
|
509
|
+
shippingInfoBody: "",
|
|
510
|
+
noReturnPolicyBody: true,
|
|
511
|
+
noShippingInfoBody: true,
|
|
512
|
+
returnPolicyCollapsed: true,
|
|
513
|
+
shippingInfoCollapsed: true,
|
|
514
|
+
noSaveHint: true,
|
|
515
|
+
saveIcon: "heart",
|
|
516
|
+
optionsSummary: "",
|
|
517
|
+
quantityLabel: "Qty: 1",
|
|
518
|
+
quantityIsOne: true,
|
|
519
|
+
addedSheetPresented: false,
|
|
520
|
+
shareHint: "",
|
|
521
|
+
noShareHint: true,
|
|
522
|
+
noCartHint: true,
|
|
523
|
+
hideBar: true,
|
|
524
|
+
hideProduct: true,
|
|
386
525
|
description: "",
|
|
387
526
|
descriptionJson: "",
|
|
388
527
|
hasDescriptionJson: false,
|
|
528
|
+
returnPolicyExpanded: false,
|
|
529
|
+
shippingInfoExpanded: false,
|
|
389
530
|
image: "",
|
|
390
531
|
gallery: [],
|
|
391
532
|
hasGallery: false,
|
|
@@ -419,11 +560,14 @@ const svc = defineService<S, {
|
|
|
419
560
|
optionsOverflow: false,
|
|
420
561
|
noOptionsOverflow: true,
|
|
421
562
|
optionsIncomplete: false,
|
|
563
|
+
showOptionErrors: false,
|
|
564
|
+
addDisabled: false,
|
|
422
565
|
optionsUnknown: false,
|
|
423
566
|
optionsHint: "",
|
|
424
|
-
opt1Label: "", opt1Chips: [], opt1Selected: [], noOpt1: true,
|
|
425
|
-
opt2Label: "", opt2Chips: [], opt2Selected: [], noOpt2: true,
|
|
426
|
-
opt3Label: "", opt3Chips: [], opt3Selected: [], noOpt3: true,
|
|
567
|
+
opt1Label: "", opt1Chips: [], opt1Selected: [], opt1SelectedId: "", opt1Placeholder: "", noOpt1: true,
|
|
568
|
+
opt2Label: "", opt2Chips: [], opt2Selected: [], opt2SelectedId: "", opt2Placeholder: "", noOpt2: true,
|
|
569
|
+
opt3Label: "", opt3Chips: [], opt3Selected: [], opt3SelectedId: "", opt3Placeholder: "", noOpt3: true,
|
|
570
|
+
opt1Error: "", noOpt1Error: true, hasOpt1Error: false, opt2Error: "", noOpt2Error: true, hasOpt2Error: false, opt3Error: "", noOpt3Error: true, hasOpt3Error: false,
|
|
427
571
|
cartHidden: false,
|
|
428
572
|
buyHidden: true,
|
|
429
573
|
buyLabel: "",
|
|
@@ -441,7 +585,7 @@ const svc = defineService<S, {
|
|
|
441
585
|
// Storage problems must not prevent browsing or buying a product.
|
|
442
586
|
let saved = false;
|
|
443
587
|
try { saved = isProductSaved(id); } catch { /* save action reports a retryable error */ }
|
|
444
|
-
s = { ...s, saved, saveHint: "", adding: false, added: false };
|
|
588
|
+
s = { ...s, saved, saveHint: "", shareHint: "", adding: false, added: false, addedSheetPresented: false, quantity: 1, showOptionErrors: false, returnPolicyExpanded: false, shippingInfoExpanded: false };
|
|
445
589
|
// A fresh navigation into the product resets any quantity left over
|
|
446
590
|
// from a previous visit to the same worker instance.
|
|
447
591
|
if (!id) {
|
|
@@ -497,6 +641,7 @@ const svc = defineService<S, {
|
|
|
497
641
|
// A group with exactly one choice is chosen for the shopper; a real choice
|
|
498
642
|
// is left blank so the pick is deliberate.
|
|
499
643
|
const optionGroups = productOptions(p);
|
|
644
|
+
const sale = productSale(p);
|
|
500
645
|
// A pooled screen re-appearing re-runs `load`. Wiping `selection` there means a
|
|
501
646
|
// trip to the cart and back silently clears the size the shopper picked and
|
|
502
647
|
// re-disables the CTA with no explanation. Carry the picks when this is the SAME
|
|
@@ -518,8 +663,11 @@ const svc = defineService<S, {
|
|
|
518
663
|
selection: { ...defaultSelection(optionGroups), ...carried },
|
|
519
664
|
optionsUnknown: false,
|
|
520
665
|
name,
|
|
521
|
-
|
|
522
|
-
commercePrice:
|
|
666
|
+
...sale,
|
|
667
|
+
commercePrice: sale.price,
|
|
668
|
+
skuLabel: skuOf(p) ? `SKU: ${skuOf(p)}` : "",
|
|
669
|
+
returnPolicyBody: productPolicyBody(p, "returns") || copyParam("product.returnPolicyBody", "") || copyParam("product.policyUnavailable", "Policy details have not been provided by this store."),
|
|
670
|
+
shippingInfoBody: productPolicyBody(p, "shipping") || copyParam("product.shippingInfoBody", "") || copyParam("product.policyUnavailable", "Policy details have not been provided by this store."),
|
|
523
671
|
...productBody(p),
|
|
524
672
|
image,
|
|
525
673
|
gallery: productGallery(p),
|
|
@@ -550,6 +698,10 @@ const svc = defineService<S, {
|
|
|
550
698
|
optionsUnknown: true,
|
|
551
699
|
name,
|
|
552
700
|
price: String(synced.subtitle ?? ""),
|
|
701
|
+
wasPrice: "",
|
|
702
|
+
skuLabel: "",
|
|
703
|
+
returnPolicyBody: "",
|
|
704
|
+
shippingInfoBody: "",
|
|
553
705
|
description: "",
|
|
554
706
|
descriptionJson: "",
|
|
555
707
|
image,
|
|
@@ -569,6 +721,8 @@ const svc = defineService<S, {
|
|
|
569
721
|
const quantity = parseQuantity(argValue(arg, "value"));
|
|
570
722
|
return quantity ? derive({ ...s, quantity }) : s;
|
|
571
723
|
},
|
|
724
|
+
toggleReturnPolicy: s => derive({ ...s, returnPolicyExpanded: !s.returnPolicyExpanded }),
|
|
725
|
+
toggleShippingInfo: s => derive({ ...s, shippingInfoExpanded: !s.shippingInfoExpanded }),
|
|
572
726
|
// One chip row per option group. `group` is the descriptor slot ("1".."3");
|
|
573
727
|
// FilterChips sends back the WHOLE selected set, which for a single-select row
|
|
574
728
|
// is either one id or none (re-tapping the active chip clears it).
|
|
@@ -576,16 +730,17 @@ const svc = defineService<S, {
|
|
|
576
730
|
const slot = Math.floor(Number(argValue(arg, "group")));
|
|
577
731
|
const group = s.optionGroups[slot - 1];
|
|
578
732
|
if (!group) return s;
|
|
579
|
-
const raw = (arg as { selectedIds?: unknown } | undefined)
|
|
580
|
-
const
|
|
733
|
+
const raw = (arg as { selectedIds?: unknown; selectedId?: unknown; id?: unknown; value?: unknown } | undefined);
|
|
734
|
+
const selected = raw?.selectedIds;
|
|
735
|
+
const picked = Array.isArray(selected) ? String(selected[0] ?? "") : String(raw?.selectedId ?? raw?.id ?? raw?.value ?? selected ?? "");
|
|
581
736
|
// Only a choice this group actually offers — a stale id from a previous
|
|
582
737
|
// product would otherwise resolve to no variant and block the cart silently.
|
|
583
|
-
const choice = group.choices.some((c) => c.id === picked) ? picked : "";
|
|
738
|
+
const choice = group.choices.some((c) => c.id === picked && c.inStock !== false) ? picked : "";
|
|
584
739
|
const selection = { ...s.selection };
|
|
585
740
|
if (choice) selection[group.id] = choice; else delete selection[group.id];
|
|
586
741
|
// A new choice invalidates the last add's confirmation — it was for the
|
|
587
742
|
// previous variant.
|
|
588
|
-
return derive({ ...s, selection, added: false, cartHint: "" });
|
|
743
|
+
return derive({ ...s, selection, added: false, addedSheetPresented: false, cartHint: "" });
|
|
589
744
|
},
|
|
590
745
|
// The pending half of addToCart, as its own reducer so it PUBLISHES before the
|
|
591
746
|
// network write starts: the pending line shows and the top-bar badge ticks up in the same
|
|
@@ -594,7 +749,7 @@ const svc = defineService<S, {
|
|
|
594
749
|
beginAdd: (s, arg) => {
|
|
595
750
|
const quantity = parseQuantity(argValue(arg, "quantity")) ?? 1;
|
|
596
751
|
cartBadgeEpoch += 1;
|
|
597
|
-
return derive({ ...s, adding: true, added: false, cartHint: "", cartCount: s.cartCount + quantity });
|
|
752
|
+
return derive({ ...s, adding: true, added: false, addedSheetPresented: false, cartHint: "", cartCount: s.cartCount + quantity });
|
|
598
753
|
},
|
|
599
754
|
// The first paint from the list's synced row (called from load, not from the descriptor):
|
|
600
755
|
// name, price and image are on screen while the live product — description, gallery — loads.
|
|
@@ -641,7 +796,7 @@ const svc = defineService<S, {
|
|
|
641
796
|
if (s.optionsIncomplete) {
|
|
642
797
|
// With no groups in hand there is nothing for `optionsHint` to name, so the
|
|
643
798
|
// offline case needs its own reason rather than an empty string.
|
|
644
|
-
return derive({ ...s, cartHint: s.optionsUnknown
|
|
799
|
+
return derive({ ...s, showOptionErrors: true, cartHint: s.optionsUnknown
|
|
645
800
|
? copyParam("product.cartHintOffline", "Reconnect to choose options and add this to your cart.")
|
|
646
801
|
: optionsHint(s.optionGroups, s.selection) || copyParam("product.cartHintNoVariant", "That combination is not available. Try another.") });
|
|
647
802
|
}
|
|
@@ -668,7 +823,10 @@ const svc = defineService<S, {
|
|
|
668
823
|
// then shows one line for two adds. Sorted so the key is stable whatever order the
|
|
669
824
|
// groups were picked in, and reduced to id-safe characters.
|
|
670
825
|
const variantKey = Object.keys(s.selection).sort().map((k) => `${k}:${s.selection[k]}`).join("|").replace(/[^A-Za-z0-9]+/g, "_");
|
|
671
|
-
const
|
|
826
|
+
const unitPrice = s.commercePrice || s.price;
|
|
827
|
+
const sale = lastProduct ? productSale(lastProduct) : { wasPrice: "" };
|
|
828
|
+
const swatches = s.optionGroups.flatMap(g => g.kind === "color" ? g.choices.map(c => c.colorCode).filter(Boolean) : []).slice(0, 5);
|
|
829
|
+
const line: DbEntity = { id: variantKey ? `line-${id}-${variantKey}` : `line-${id}`, productId: id, name: s.name || "product", title: s.name || "product", subtitle: chosen ? `${chosen} · Qty ${quantity}` : `Qty ${quantity}`, quantity: String(quantity), image: s.image, unitPrice, wasPrice: sale.wasPrice, swatches, total: optimisticLineTotal(unitPrice, quantity), hasImage: !!s.image, noImage: !s.image };
|
|
672
830
|
await cartPut(line);
|
|
673
831
|
try {
|
|
674
832
|
await addToCurrentCart(id, quantity, cartOptions);
|
|
@@ -685,15 +843,18 @@ const svc = defineService<S, {
|
|
|
685
843
|
// The write LANDED. The badge re-read is outside the write's try on purpose: a failed
|
|
686
844
|
// count must never be mistaken for a failed add. Server count wins; else keep optimistic.
|
|
687
845
|
const count = await cartItemCount().catch(() => 0);
|
|
688
|
-
|
|
846
|
+
const confirmation = copyTemplate("product.cartHintAdded", "Added {name} to cart.", { name: s.name || "product" });
|
|
847
|
+
return derive({ ...s, adding: false, added: true, addedSheetPresented: true, cartHint: `${confirmation}${quantity > 1 ? ` Qty: ${quantity}` : ""}`, cartCount: count || s.cartCount });
|
|
689
848
|
},
|
|
690
849
|
toggleSave: (s) => {
|
|
691
850
|
if (s.status !== "ready" || !s.name || !inputId()) return s;
|
|
692
851
|
try {
|
|
693
|
-
const
|
|
852
|
+
const sale = lastProduct ? productSale(lastProduct) : { price: s.commercePrice ?? "", wasPrice: "" };
|
|
853
|
+
const saved = toggleSavedProduct({ id: inputId(), name: s.name, price: sale.price, wasPrice: sale.wasPrice, image: s.image,
|
|
854
|
+
swatches: s.optionGroups.flatMap(g => g.kind === "color" ? g.choices.map(c => c.colorCode).filter(Boolean) : []).slice(0, 5) });
|
|
694
855
|
return derive({ ...s, saved, saveHint: saved
|
|
695
|
-
? copyParam("product.saveSuccess", "
|
|
696
|
-
: copyParam("product.saveRemoved", "Removed from
|
|
856
|
+
? copyParam("product.saveSuccess", "Added to wishlist.")
|
|
857
|
+
: copyParam("product.saveRemoved", "Removed from wishlist.") });
|
|
697
858
|
} catch {
|
|
698
859
|
return derive({ ...s, saveHint: copyParam("product.saveError", "Could not save this change. Try again.") });
|
|
699
860
|
}
|
|
@@ -702,12 +863,13 @@ const svc = defineService<S, {
|
|
|
702
863
|
if (!s.name) return s;
|
|
703
864
|
try {
|
|
704
865
|
await shareSheet({ subject: s.name, text: s.name, ...(s.shareUrl ? { url: s.shareUrl } : {}) });
|
|
705
|
-
return s;
|
|
866
|
+
return derive({ ...s, shareHint: "" });
|
|
706
867
|
} catch {
|
|
707
868
|
// `added: false` — this hint must not render inside the add-to-cart confirmation.
|
|
708
|
-
return derive({ ...s,
|
|
869
|
+
return derive({ ...s, shareHint: copyParam("product.shareUnavailable", "Sharing is not available on this device.") });
|
|
709
870
|
}
|
|
710
871
|
},
|
|
872
|
+
closeAddedSheet: s => derive({ ...s, addedSheetPresented: false }),
|
|
711
873
|
// Only http(s), mailto and tel links reach open_url; a refused link, or one nothing opened, says so in the
|
|
712
874
|
// hint line (`added: false`, as for share, so it never renders inside the add-to-cart confirmation).
|
|
713
875
|
openLink: async (s, arg) => ((await openRichLink(arg))
|
|
@@ -3,7 +3,12 @@ import { withEmptyFlags } from "../_shared/empty-flags.js";
|
|
|
3
3
|
import { readSavedProducts, removeSavedProduct, type SavedProduct } from "../_shared/himi-store-saved.js";
|
|
4
4
|
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
5
5
|
|
|
6
|
-
interface Row extends SavedProduct {
|
|
6
|
+
interface Row extends SavedProduct {
|
|
7
|
+
commercePrice?: string; initial: string; noImage: boolean; hasImage: boolean; detailScreenId: string;
|
|
8
|
+
notOnSale: boolean; wasPriceEmpty: boolean; noSwatches: boolean; noTagRow: boolean;
|
|
9
|
+
swatch1: string; swatch2: string; swatch3: string; swatch4: string; swatch5: string;
|
|
10
|
+
noSwatch1: boolean; noSwatch2: boolean; noSwatch3: boolean; noSwatch4: boolean; noSwatch5: boolean;
|
|
11
|
+
}
|
|
7
12
|
interface S {
|
|
8
13
|
status: "loading" | "ready" | "error";
|
|
9
14
|
rows: Row[];
|
|
@@ -13,20 +18,31 @@ interface S {
|
|
|
13
18
|
notLoading: boolean;
|
|
14
19
|
notErrored: boolean;
|
|
15
20
|
noEmpty: boolean;
|
|
21
|
+
hasRows: boolean;
|
|
16
22
|
}
|
|
17
23
|
function shape(s: Pick<S, "status" | "rows" | "error">): S {
|
|
18
|
-
return { ...s, isLoading: s.status === "loading", notLoading: s.status !== "loading", notErrored: s.status !== "error", noEmpty: s.status !== "ready" || s.rows.length > 0, emptyText: copyParam("savedProducts.empty", "
|
|
24
|
+
return { ...s, isLoading: s.status === "loading", notLoading: s.status !== "loading", notErrored: s.status !== "error", noEmpty: s.status !== "ready" || s.rows.length > 0, hasRows: s.rows.length > 0, emptyText: copyParam("savedProducts.empty", "Tap the heart on a product to add it to your wishlist.") };
|
|
19
25
|
}
|
|
20
26
|
function loadRows(): Row[] {
|
|
21
|
-
return readSavedProducts().map(row =>
|
|
27
|
+
return readSavedProducts().map(row => {
|
|
28
|
+
const swatches = (row.swatches ?? []).filter(value => /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(value)).slice(0, 5);
|
|
29
|
+
return { ...row, commercePrice: row.price, initial: row.name.slice(0, 1).toUpperCase(), hasImage: !!row.image, noImage: !row.image,
|
|
30
|
+
detailScreenId: "himi_product", notOnSale: !row.wasPrice, wasPriceEmpty: !!row.wasPrice, noSwatches: swatches.length === 0, noTagRow: !row.wasPrice && swatches.length === 0,
|
|
31
|
+
swatch1: swatches[0] ?? "", swatch2: swatches[1] ?? "", swatch3: swatches[2] ?? "", swatch4: swatches[3] ?? "", swatch5: swatches[4] ?? "",
|
|
32
|
+
noSwatch1: !swatches[0], noSwatch2: !swatches[1], noSwatch3: !swatches[2], noSwatch4: !swatches[3], noSwatch5: !swatches[4] };
|
|
33
|
+
});
|
|
22
34
|
}
|
|
23
35
|
function failed(s: S): S {
|
|
24
|
-
return shape({ ...s, status: "error", error: copyParam("savedProducts.error", "Could not update
|
|
36
|
+
return shape({ ...s, status: "error", error: copyParam("savedProducts.error", "Could not update your wishlist. Try again.") });
|
|
25
37
|
}
|
|
26
38
|
const defineService = withIapPresentation<S>({
|
|
27
39
|
prefix: "savedProducts", transientKeys: ["rows"], contentActions: ["load", "remove"],
|
|
28
40
|
items: state => state.rows.map(row => ({ reference: { kind: "storesProduct", id: row.id }, commerceDisplayPrice: row.commercePrice ?? row.price })),
|
|
29
|
-
present: (state, price) => ({ ...state, rows: state.rows.map(row =>
|
|
41
|
+
present: (state, price) => ({ ...state, rows: state.rows.map(row => {
|
|
42
|
+
const next = price(row.id, row.commercePrice ?? row.price);
|
|
43
|
+
const iapAdjusted = next !== (row.commercePrice ?? row.price);
|
|
44
|
+
return { ...row, price: next, wasPriceEmpty: !!row.wasPrice && !iapAdjusted, notOnSale: !row.wasPrice || iapAdjusted, noTagRow: (!row.wasPrice || iapAdjusted) && !row.swatches?.length };
|
|
45
|
+
}) }),
|
|
30
46
|
});
|
|
31
47
|
const svc = defineService<S, {
|
|
32
48
|
load: (s: S, arg?: unknown) => S;
|
|
@@ -39,6 +39,7 @@ interface Row {
|
|
|
39
39
|
name: string;
|
|
40
40
|
subtitle: string;
|
|
41
41
|
commercePrice: string;
|
|
42
|
+
wasPrice: string;
|
|
42
43
|
image: string;
|
|
43
44
|
initial: string;
|
|
44
45
|
hasImage: boolean;
|
|
@@ -73,11 +74,22 @@ function row(p: StoreProduct): Row {
|
|
|
73
74
|
const name = cleanTitle(p.name) || "Product";
|
|
74
75
|
const image = productImage(p);
|
|
75
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 : "";
|
|
76
87
|
return {
|
|
77
88
|
id,
|
|
78
89
|
name,
|
|
79
|
-
subtitle:
|
|
80
|
-
commercePrice:
|
|
90
|
+
subtitle: salePrice || regular,
|
|
91
|
+
commercePrice: salePrice || regular,
|
|
92
|
+
wasPrice,
|
|
81
93
|
image,
|
|
82
94
|
initial: initials(name),
|
|
83
95
|
hasImage: !!image,
|
|
@@ -35,9 +35,17 @@
|
|
|
35
35
|
"body": { "size": 16, "weight": 400, "line": 1.5, "letterSpacing": 0 },
|
|
36
36
|
"bodyEmphasis": { "size": 16, "weight": 700, "line": 1.35, "letterSpacing": 0 },
|
|
37
37
|
"footnote": { "size": 14, "weight": 500, "line": 1.35, "letterSpacing": 0 },
|
|
38
|
+
"footnoteRegular": { "size": 14, "weight": 400, "line": 1.4, "letterSpacing": 1 },
|
|
38
39
|
"caption": { "size": 13, "weight": 400, "line": 1.4, "letterSpacing": 0 },
|
|
39
40
|
"display": { "size": 34, "weight": 800, "line": 1.1, "letterSpacing": 0 }
|
|
40
41
|
},
|
|
42
|
+
"controls": {
|
|
43
|
+
"button": {
|
|
44
|
+
"variants": {
|
|
45
|
+
"tertiary": { "label": "textPrimary" }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
41
49
|
"radius": {
|
|
42
50
|
"sm": 4,
|
|
43
51
|
"md": 8,
|
|
@@ -5,6 +5,8 @@ export interface SavedProduct {
|
|
|
5
5
|
name: string;
|
|
6
6
|
price: string;
|
|
7
7
|
image: string;
|
|
8
|
+
wasPrice?: string;
|
|
9
|
+
swatches?: string[];
|
|
8
10
|
}
|
|
9
11
|
type StorageLike = { getItem(key: string): string | null; setItem(key: string, value: string): void };
|
|
10
12
|
const KEY = "himi-store:saved-products:v1";
|
|
@@ -24,7 +26,11 @@ export function readSavedProducts(): SavedProduct[] {
|
|
|
24
26
|
if (!value || typeof value !== "object") continue;
|
|
25
27
|
const row = value as Record<string, unknown>;
|
|
26
28
|
if (typeof row.id !== "string" || !row.id.trim() || typeof row.name !== "string") continue;
|
|
27
|
-
|
|
29
|
+
const swatches = Array.isArray(row.swatches)
|
|
30
|
+
? row.swatches.filter((color): color is string => typeof color === "string" && /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(color)).slice(0, 5)
|
|
31
|
+
: [];
|
|
32
|
+
rows.set(row.id, { id: row.id, name: row.name, price: typeof row.price === "string" ? row.price : "", image: typeof row.image === "string" ? row.image : "",
|
|
33
|
+
...(typeof row.wasPrice === "string" && row.wasPrice ? { wasPrice: row.wasPrice } : {}), ...(swatches.length ? { swatches } : {}) });
|
|
28
34
|
}
|
|
29
35
|
return [...rows.values()];
|
|
30
36
|
}
|
|
@@ -161,6 +161,12 @@
|
|
|
161
161
|
"letterSpacing": 0.1,
|
|
162
162
|
"family": "Wix Madefor Text"
|
|
163
163
|
},
|
|
164
|
+
"footnoteRegular": {
|
|
165
|
+
"size": 14,
|
|
166
|
+
"weight": 400,
|
|
167
|
+
"line": 1.4,
|
|
168
|
+
"letterSpacing": 1
|
|
169
|
+
},
|
|
164
170
|
"caption": {
|
|
165
171
|
"size": 13,
|
|
166
172
|
"weight": 600,
|
|
@@ -183,6 +189,15 @@
|
|
|
183
189
|
"family": "Wix Madefor Display"
|
|
184
190
|
}
|
|
185
191
|
},
|
|
192
|
+
"controls": {
|
|
193
|
+
"button": {
|
|
194
|
+
"variants": {
|
|
195
|
+
"tertiary": {
|
|
196
|
+
"label": "textPrimary"
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
},
|
|
186
201
|
"radius": {
|
|
187
202
|
"sm": 18,
|
|
188
203
|
"md": 24,
|
|
@@ -5,6 +5,8 @@ export interface SavedProduct {
|
|
|
5
5
|
name: string;
|
|
6
6
|
price: string;
|
|
7
7
|
image: string;
|
|
8
|
+
wasPrice?: string;
|
|
9
|
+
swatches?: string[];
|
|
8
10
|
}
|
|
9
11
|
type StorageLike = { getItem(key: string): string | null; setItem(key: string, value: string): void };
|
|
10
12
|
const KEY = "himi-store:saved-products:v1";
|
|
@@ -24,7 +26,11 @@ export function readSavedProducts(): SavedProduct[] {
|
|
|
24
26
|
if (!value || typeof value !== "object") continue;
|
|
25
27
|
const row = value as Record<string, unknown>;
|
|
26
28
|
if (typeof row.id !== "string" || !row.id.trim() || typeof row.name !== "string") continue;
|
|
27
|
-
|
|
29
|
+
const swatches = Array.isArray(row.swatches)
|
|
30
|
+
? row.swatches.filter((color): color is string => typeof color === "string" && /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(color)).slice(0, 5)
|
|
31
|
+
: [];
|
|
32
|
+
rows.set(row.id, { id: row.id, name: row.name, price: typeof row.price === "string" ? row.price : "", image: typeof row.image === "string" ? row.image : "",
|
|
33
|
+
...(typeof row.wasPrice === "string" && row.wasPrice ? { wasPrice: row.wasPrice } : {}), ...(swatches.length ? { swatches } : {}) });
|
|
28
34
|
}
|
|
29
35
|
return [...rows.values()];
|
|
30
36
|
}
|