@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.
- 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 -30
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/sdk-reference.json +8 -2
- 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/beauty-salon-owner/dev/index.ts +571 -0
- package/templates/beauty-salon-owner/himalaya.content.json +6 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/branded-lite/dev/index.ts +2989 -2197
- package/templates/branded-lite/tier5-src/_shared/business-chat-presenter.ts +46 -8
- 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/business-chat/index.ts +3 -2
- 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/community-club-owner/dev/index.ts +571 -0
- package/templates/community-club-owner/himalaya.content.json +6 -0
- package/templates/community-club-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/community-club-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/community-club-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/community-club-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- 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/events-meetups-owner/dev/index.ts +571 -0
- package/templates/events-meetups-owner/himalaya.content.json +6 -0
- package/templates/events-meetups-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/events-meetups-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- 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/gym-fitness-owner/dev/index.ts +571 -0
- package/templates/gym-fitness-owner/himalaya.content.json +6 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -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/nonprofit-cause-owner/dev/index.ts +571 -0
- package/templates/nonprofit-cause-owner/himalaya.content.json +6 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- package/templates/owner-lite/dev/index.ts +571 -0
- package/templates/owner-lite/himalaya.content.json +6 -0
- package/templates/owner-lite/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/owner-lite/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/owner-lite/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/owner-lite/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- 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/podcast-creator-owner/dev/index.ts +571 -0
- package/templates/podcast-creator-owner/himalaya.content.json +6 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- 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/portfolio-creative-owner/dev/index.ts +571 -0
- package/templates/portfolio-creative-owner/himalaya.content.json +6 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -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/restaurant-owner/dev/index.ts +571 -0
- package/templates/restaurant-owner/himalaya.content.json +6 -0
- package/templates/restaurant-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/restaurant-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- 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/retail-store-owner/dev/index.ts +571 -0
- package/templates/retail-store-owner/himalaya.content.json +6 -0
- package/templates/retail-store-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/retail-store-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -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/yoga-studio-owner/dev/index.ts +571 -0
- package/templates/yoga-studio-owner/himalaya.content.json +6 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-today-rollup.ts +307 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/owner-widget-reads.ts +23 -0
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-today/index.ts +8 -20
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-today-rollup/index.ts +121 -0
- 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
|
@@ -11,46 +11,189 @@ export function withIapPresentation<S extends object>(options: {
|
|
|
11
11
|
onCreate?(commerce: IapCommerce): void;
|
|
12
12
|
onContent?(state: S): void;
|
|
13
13
|
items(state: S): IapCatalogItem[];
|
|
14
|
-
present(state: S, price: (id: string, commercePrice?: string) => string, allowed: (id: string) => boolean): S;
|
|
14
|
+
present(state: S, price: (id: string, commercePrice?: string) => string, allowed: (id: string) => boolean, appleIapOrResolving: (id: string) => boolean): S;
|
|
15
15
|
contentActions: string[];
|
|
16
|
+
/** Defaults to load/refresh/retry; mutations can explicitly request fresh mapping. */
|
|
17
|
+
forcePriceRefreshActions?: string[];
|
|
18
|
+
concurrentPublishActions?: string[];
|
|
19
|
+
/** Override the commerce factory in focused worker tests. */
|
|
20
|
+
commerceFactory?: typeof createIapCommerce;
|
|
16
21
|
transientKeys: (keyof S & string)[];
|
|
17
22
|
prefix: string;
|
|
18
23
|
}): typeof defineService {
|
|
19
24
|
return ((name: string, definition: ServiceDefinition<S>) => {
|
|
20
|
-
|
|
25
|
+
let queuePublish = () => {};
|
|
26
|
+
const commerce = (options.commerceFactory ?? createIapCommerce)({ adapters: options.adapters, onChange: () => queuePublish() });
|
|
21
27
|
options.onCreate?.(commerce);
|
|
22
28
|
let items: IapCatalogItem[] = [];
|
|
29
|
+
let loadedItemsKey = "";
|
|
30
|
+
const settledViews = new Map<string, { commerceDisplayPrice?: string; view: ReturnType<IapCommerce["pricing"]["view"]> }>();
|
|
31
|
+
const forcePriceRefreshActions = new Set(options.forcePriceRefreshActions ?? options.contentActions.filter(action => ["load", "refresh", "retry"].includes(action)));
|
|
23
32
|
let contentVersion = 0;
|
|
24
|
-
|
|
33
|
+
let actionVersion = 0;
|
|
34
|
+
let activeActions = 0;
|
|
35
|
+
let activeConcurrentPublishActions = 0;
|
|
36
|
+
let actionWaiters: Array<() => void> = [];
|
|
37
|
+
let publishQueued = false;
|
|
38
|
+
// pollDirective() arms its interval as a side effect. Keep a directive from a
|
|
39
|
+
// projection that loses a commit race so the retry can still publish it.
|
|
40
|
+
let uncommittedPollDirective: ReturnType<IapCommerce["pollDirective"]>;
|
|
41
|
+
let uncommittedPollVersion = 0;
|
|
42
|
+
// The committed state, read when a publish runs. Assigned once the service exists, so the
|
|
43
|
+
// service's own initializer never refers to it.
|
|
44
|
+
let committed: () => S = () => definition.state;
|
|
45
|
+
queuePublish = () => {
|
|
46
|
+
if (publishQueued) return;
|
|
47
|
+
publishQueued = true;
|
|
48
|
+
// Tier-5 workers have no timer globals. A microtask is enough here: if the
|
|
49
|
+
// action that caused the price update is still in flight, iapPublish's
|
|
50
|
+
// action-version check queues another publish after that action commits.
|
|
51
|
+
const publish = () => {
|
|
52
|
+
publishQueued = false;
|
|
53
|
+
void service.iapPublish();
|
|
54
|
+
};
|
|
55
|
+
void Promise.resolve().then(publish);
|
|
56
|
+
};
|
|
57
|
+
const project = (state: S, retainedPoll?: ReturnType<IapCommerce["pollDirective"]>): S => {
|
|
58
|
+
const presentationView = (id: string, commercePrice?: string) => {
|
|
59
|
+
const item = items.find(item => item.reference.id === id);
|
|
60
|
+
const reference = item?.reference ?? { kind: "storesProduct", id };
|
|
61
|
+
const key = `${reference.kind}:${reference.id}`;
|
|
62
|
+
const current = commerce.pricing.view(reference);
|
|
63
|
+
const cached = settledViews.get(key);
|
|
64
|
+
const displayPrice = commercePrice ?? item?.commerceDisplayPrice;
|
|
65
|
+
if (current.route === "resolving" && cached && cached.commerceDisplayPrice === displayPrice) return cached.view;
|
|
66
|
+
if (current.route !== "resolving" && item) settledViews.set(key, { commerceDisplayPrice: item.commerceDisplayPrice, view: current });
|
|
67
|
+
return current;
|
|
68
|
+
};
|
|
25
69
|
const price = (id: string, commercePrice?: string) => {
|
|
26
70
|
const item = items.find(item => item.reference.id === id);
|
|
27
71
|
if (!item) return "";
|
|
28
|
-
const view =
|
|
72
|
+
const view = presentationView(id, commercePrice);
|
|
73
|
+
if (view.route === "resolving") return iapPriceCopy(options.prefix).loading;
|
|
29
74
|
return view.route === "disabled" || view.route === "regular" ? commercePrice ?? item.commerceDisplayPrice ?? "" : iapPriceFields(view, iapPriceCopy(options.prefix)).priceText;
|
|
30
75
|
};
|
|
31
76
|
const allowed = (id: string) => commerce.pricing.checkoutGuard({ kind: "storesProduct", id }).allowed;
|
|
32
|
-
const
|
|
33
|
-
|
|
77
|
+
const appleIapOrResolving = (id: string) => ["appleIap", "resolving", "unavailable"].includes(presentationView(id).route);
|
|
78
|
+
// Re-evaluate the cadence first. A checkout can enter/leave due work while a prior
|
|
79
|
+
// projection is waiting to retry; a fresh directive supersedes the retained one.
|
|
80
|
+
const poll = commerce.pollDirective(`${name}.iapPoll`) ?? retainedPoll;
|
|
81
|
+
return { ...options.present(state, price, allowed, appleIapOrResolving), ...(poll ? { _poll: poll } : {}) };
|
|
34
82
|
};
|
|
35
|
-
const
|
|
83
|
+
const mergeConcurrent = (before: S, next: S, current: S): S => {
|
|
84
|
+
if (current === before) return next;
|
|
85
|
+
const merged = { ...current } as Record<string, unknown>;
|
|
86
|
+
const previous = before as Record<string, unknown>;
|
|
87
|
+
const changed = next as Record<string, unknown>;
|
|
88
|
+
for (const key of new Set([...Object.keys(previous), ...Object.keys(changed)])) {
|
|
89
|
+
if (JSON.stringify(previous[key]) === JSON.stringify(changed[key])) continue;
|
|
90
|
+
if (Object.prototype.hasOwnProperty.call(changed, key)) merged[key] = changed[key];
|
|
91
|
+
else delete merged[key];
|
|
92
|
+
}
|
|
93
|
+
return merged as S;
|
|
94
|
+
};
|
|
95
|
+
const actions = Object.fromEntries(Object.entries(definition.actions ?? {}).map(([action, reduce]) => [action, (state: S, ...args: never[]) => {
|
|
96
|
+
actionVersion++;
|
|
97
|
+
activeActions++;
|
|
98
|
+
const concurrentPublish = options.concurrentPublishActions?.includes(action) ?? false;
|
|
99
|
+
if (concurrentPublish) activeConcurrentPublishActions++;
|
|
36
100
|
const content = options.contentActions.includes(action);
|
|
37
101
|
const version = action === "load" ? ++contentVersion : contentVersion;
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
102
|
+
const presentResult = (next: S | undefined) => {
|
|
103
|
+
// scopeLanded is the completion of the active load, not a stale load result. It
|
|
104
|
+
// must publish even if another overlapping onAppear load advanced contentVersion.
|
|
105
|
+
if (!next || (content && action !== "scopeLanded" && version !== contentVersion)) return undefined;
|
|
106
|
+
if (content) {
|
|
107
|
+
options.onContent?.(next);
|
|
108
|
+
const nextItems = options.items(next);
|
|
109
|
+
// This is an order-independent identity key, not display text. Default
|
|
110
|
+
// string ordering is deterministic on every worker engine, including QuickJS.
|
|
111
|
+
const nextItemsKey = JSON.stringify(nextItems.map(item => JSON.stringify([item.reference.kind, item.reference.id, item.commerceDisplayPrice]))
|
|
112
|
+
.sort());
|
|
113
|
+
items = nextItems;
|
|
114
|
+
const forcePriceRefresh = forcePriceRefreshActions.has(action);
|
|
115
|
+
if (forcePriceRefresh) settledViews.clear();
|
|
116
|
+
if (nextItemsKey !== loadedItemsKey || forcePriceRefresh) {
|
|
117
|
+
loadedItemsKey = nextItemsKey;
|
|
118
|
+
void commerce.loadPrices(items);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Checkout preserves prices published while it ran.
|
|
122
|
+
return project(concurrentPublish ? mergeConcurrent(state, next, committed()) : next);
|
|
123
|
+
};
|
|
124
|
+
const finished = () => {
|
|
125
|
+
activeActions--;
|
|
126
|
+
if (concurrentPublish) activeConcurrentPublishActions--;
|
|
127
|
+
const waiters = actionWaiters;
|
|
128
|
+
actionWaiters = [];
|
|
129
|
+
for (const resolve of waiters) resolve();
|
|
130
|
+
};
|
|
131
|
+
try {
|
|
132
|
+
const next = reduce(state, ...args);
|
|
133
|
+
// Keep synchronous sidecars synchronous. Adding an await lets their old
|
|
134
|
+
// catalog snapshot commit after a newer asynchronous refresh.
|
|
135
|
+
if (content || (next && typeof (next as Promise<S | undefined>).then === "function")) {
|
|
136
|
+
return (async () => {
|
|
137
|
+
try { return presentResult(await next); }
|
|
138
|
+
finally { finished(); }
|
|
139
|
+
})();
|
|
140
|
+
}
|
|
141
|
+
const result = presentResult(next as S | undefined);
|
|
142
|
+
finished();
|
|
143
|
+
return result;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
finished();
|
|
146
|
+
throw error;
|
|
44
147
|
}
|
|
45
|
-
return project(next);
|
|
46
148
|
}]));
|
|
47
149
|
const service = (options.register ?? defineService)(name, {
|
|
48
150
|
// The collection's shown page goes with the rows and chrome it copies: kept out of the restore with them.
|
|
49
151
|
...definition, notHydrated: [...(definition.notHydrated ?? []), ...options.transientKeys,
|
|
50
152
|
...((options.transientKeys as string[]).includes('collection') ? ['collectionShown' as keyof S & string] : [])],
|
|
51
|
-
|
|
153
|
+
// Prices land on their own clock, and a commit replaces the whole state — so a publish
|
|
154
|
+
// can carry a snapshot that an in-flight commit (a page that just landed, a tap) has
|
|
155
|
+
// since replaced. Project the state as it is now, and publish only when the prices
|
|
156
|
+
// actually change it: with no App Store mapping, or prices already shown, there is
|
|
157
|
+
// nothing to publish and nothing to overwrite.
|
|
158
|
+
actions: { ...actions, iapPublish: async () => {
|
|
159
|
+
// Checkout actions publish their busy phase while running. Every other action
|
|
160
|
+
// finishes before we take a projection, so it cannot erase a reducer's result.
|
|
161
|
+
for (;;) {
|
|
162
|
+
if (activeActions > activeConcurrentPublishActions) await new Promise<void>(resolve => actionWaiters.push(resolve));
|
|
163
|
+
await Promise.resolve();
|
|
164
|
+
const version = actionVersion;
|
|
165
|
+
await Promise.resolve();
|
|
166
|
+
if (activeActions <= activeConcurrentPublishActions && version === actionVersion) break;
|
|
167
|
+
}
|
|
168
|
+
const current = committed();
|
|
169
|
+
// The runtime commits an action's result just after this wrapper returns. If a
|
|
170
|
+
// scope action lands while prices are being projected, publishing this snapshot
|
|
171
|
+
// would put the older scope back on top. Drop it and retry against the commit.
|
|
172
|
+
const retainedPollVersion = uncommittedPollVersion;
|
|
173
|
+
const retainedPoll = uncommittedPollDirective;
|
|
174
|
+
const next: S & { _poll?: unknown } = project(current, retainedPoll);
|
|
175
|
+
await Promise.resolve();
|
|
176
|
+
if (committed() !== current) {
|
|
177
|
+
// Overlapping retries can race too: a projection with no new directive must
|
|
178
|
+
// not erase one another already retained after pollDirective() armed itself.
|
|
179
|
+
if (next._poll) {
|
|
180
|
+
uncommittedPollDirective = next._poll as ReturnType<IapCommerce["pollDirective"]>;
|
|
181
|
+
uncommittedPollVersion++;
|
|
182
|
+
}
|
|
183
|
+
queuePublish();
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
// Another concurrent projection may have retained a newer directive since this
|
|
187
|
+
// snapshot. Clear only the exact retained generation this successful commit used.
|
|
188
|
+
if (next._poll && retainedPollVersion === uncommittedPollVersion && retainedPoll === uncommittedPollDirective) {
|
|
189
|
+
uncommittedPollDirective = undefined;
|
|
190
|
+
uncommittedPollVersion++;
|
|
191
|
+
}
|
|
192
|
+
return !next._poll && JSON.stringify(next) === JSON.stringify(current) ? undefined : next;
|
|
193
|
+
}, iapPoll: async () => { await commerce.poll(); return undefined; },
|
|
52
194
|
iapRefresh: async () => { await commerce.loadPrices(items); return undefined; } },
|
|
53
195
|
});
|
|
196
|
+
committed = () => (service as unknown as { getState(): S }).getState();
|
|
54
197
|
resetUnhydratedKeysOnOlderRuntime(service, definition.state, options.transientKeys);
|
|
55
198
|
return service;
|
|
56
199
|
}) as unknown as typeof defineService;
|
|
@@ -14,6 +14,7 @@ type PendingIntent = BusinessChatIntent;
|
|
|
14
14
|
// parts, plain text or text runs: they would be re-sent each time for nothing.
|
|
15
15
|
type BusinessMessageRow = Pick<MessageRow,'id'|'sender'|'when'|'createdDate'|'mine'> & {
|
|
16
16
|
senderAvatar:string; senderName:string; senderInitials:string; senderBadge:string; imageUrl:string; imageDescription:string; noImage:boolean; bodyText:string; bodyMarkdown:string; suggestions:BusinessSuggestion[]; articleActions:{id:string;label:string;url:string;style:'primary'|'secondary'}[]; couponCode:string; noCoupon:boolean; notMine:boolean;
|
|
17
|
+
isCard:boolean; notCard:boolean; noSuggestions:boolean; noActions:boolean; cardTitle:string; cardDetail:string; noCardText:boolean;
|
|
17
18
|
dayLabel:string; dayHidden:boolean; startsDay:boolean; scrollAnchor:string;
|
|
18
19
|
/** Only the member's own row that the server has not returned yet. */
|
|
19
20
|
pending?:boolean;
|
|
@@ -67,7 +68,7 @@ function pendingRow(intent:BusinessChatIntent):BusinessMessageRow{
|
|
|
67
68
|
const when=intent.delivery==='sending'?'Sending…':intent.delivery==='unconfirmed'?'Not confirmed':messageTimeLabel(intent.createdDate);
|
|
68
69
|
return {id:intent.id,sender:'',when,createdDate:intent.createdDate,mine:true,
|
|
69
70
|
senderAvatar:'',senderName:intent.senderName??'',senderInitials:'',senderBadge:'',imageUrl:'',imageDescription:'',noImage:true,bodyText:intent.text,bodyMarkdown:intent.text,
|
|
70
|
-
suggestions:[],articleActions:[],couponCode:'',noCoupon:true,notMine:false,dayLabel:'',dayHidden:true,startsDay:false,scrollAnchor:`himi_business_chat.message_${intent.id}`,pending:true};
|
|
71
|
+
suggestions:[],articleActions:[],couponCode:'',noCoupon:true,notMine:false,isCard:false,notCard:true,noSuggestions:true,noActions:true,cardTitle:'',cardDetail:'',noCardText:true,dayLabel:'',dayHidden:true,startsDay:false,scrollAnchor:`himi_business_chat.message_${intent.id}`,pending:true};
|
|
71
72
|
}
|
|
72
73
|
function shape(s:State):State{const used=new Set(s.usedSuggestionIds);const messages=s.messages.map(row=>({...row,suggestions:(row.suggestions||[]).map(button=>used.has(button.id)?{...button,enabled:false,disabled:true}:button)}));const quickReplies=(s.quickReplies||[]).map(button=>used.has(button.id)?{...button,enabled:false,disabled:true}:button);const quickEnabled=quickReplies.some(button=>button.enabled);return {...s,messages,quickReplies,hasQuickReplies:quickEnabled,noQuickReplies:!quickEnabled,scrollAnchor:messages[messages.length-1]?.scrollAnchor||'',liveLabel:liveDescription(s.liveState,s.liveReason),hasActiveRoom:!!s.roomId,noActiveRoom:!s.roomId,hasHostedName:!!s.hostedName,noHostedName:!s.hostedName,isGuest:s.phase==='guest',notGuest:s.phase!=='guest',realtimeDiagnostic:`state=${s.liveState}; reason=${s.liveReason||'none'}; events=${s.realtimeEventCount}; lastType=${/^(message|messagesent)$/i.test(s.lastRealtimeEventType)?s.lastRealtimeEventType:'none'}`,errorText:s.error,loadingHidden:s.phase!=='loading'||!!s.error,noError:!s.error,noNotice:!s.notice,noMessages:!messages.length,hasMessages:!!messages.length,noEmpty:s.phase!=='ready'||!!messages.length,
|
|
73
74
|
notLoading:s.phase!=='loading',sendDisabled:!canSend(s,s.draft),sendEnabled:canSend(s,s.draft),noLive:!s.roomId||s.liveState==='connected'};}
|
|
@@ -131,7 +132,7 @@ function productImageFor(slug:string,name:string):Promise<string>{
|
|
|
131
132
|
}
|
|
132
133
|
function slimRow(row:Record<string,unknown>):BusinessMessageRow{
|
|
133
134
|
const pick=(...keys:string[])=>Object.fromEntries(keys.map(key=>[key,row[key]]));
|
|
134
|
-
return pick('id','sender','when','createdDate','mine','senderAvatar','senderName','senderInitials','senderBadge','imageUrl','imageDescription','noImage','bodyText','bodyMarkdown','suggestions','articleActions','couponCode','noCoupon','notMine','dayLabel','dayHidden','startsDay','scrollAnchor') as unknown as BusinessMessageRow;
|
|
135
|
+
return pick('id','sender','when','createdDate','mine','senderAvatar','senderName','senderInitials','senderBadge','imageUrl','imageDescription','noImage','bodyText','bodyMarkdown','suggestions','articleActions','couponCode','noCoupon','notMine','isCard','notCard','noSuggestions','noActions','cardTitle','cardDetail','noCardText','dayLabel','dayHidden','startsDay','scrollAnchor') as unknown as BusinessMessageRow;
|
|
135
136
|
}
|
|
136
137
|
async function presentMessages(existing:MessageRow[],raw:unknown[],memberId?:string):Promise<BusinessMessageRow[]>{
|
|
137
138
|
const rows=chronologicalMessages(existing,raw,memberId) as MessageRow[];
|
|
@@ -2,6 +2,7 @@ import { fetchMemberBookings, type MemberBookingRow } from "../_shared/member-bo
|
|
|
2
2
|
import { hostSiteFromInput, hostedExploreCards, hostedHiddenFlags, type HostedExploreCard } from "../_shared/branded-lite-hosted.js";
|
|
3
3
|
import { memberAccess, assertMemberAccess } from "../_shared/member-access.js";
|
|
4
4
|
import { defineService as registerHome } from "../_shared/home-service.js";
|
|
5
|
+
import { navigate } from "@himalaya/state";
|
|
5
6
|
import { withIapPresentation } from "../_shared/iap-presentation.js";
|
|
6
7
|
import { queryServices, type WixService } from "@himalaya/wix-bookings";
|
|
7
8
|
import { getPost, queryPosts, type WixPost } from "@himalaya/wix-blog";
|
|
@@ -20,6 +21,7 @@ import { parseWeeklyGoal, weeklyGoalProgress, weeklySessionsFromPages } from "..
|
|
|
20
21
|
import { isBridgeUnavailable } from "../_shared/owner-load-error.js";
|
|
21
22
|
import { formatDateMedium } from "../_shared/engine-intl.js";
|
|
22
23
|
import { readSavedProducts, type SavedProduct } from "../_shared/himi-store-saved.js";
|
|
24
|
+
import { loadStoreCategories, type StoreCategoryCard } from "../_shared/himi-store-categories.js";
|
|
23
25
|
import { readSavedIds } from "../_shared/himi-blog-saved.js";
|
|
24
26
|
import { wixMediaUrl } from "@himalaya/wix-media";
|
|
25
27
|
import { isWixVideoWebRefusal, listLatestVideos, videoCard, type WixVideoCard } from "@himalaya/wix-video";
|
|
@@ -270,6 +272,9 @@ interface State {
|
|
|
270
272
|
noHeroImage: boolean;
|
|
271
273
|
hasFeaturedProducts: boolean;
|
|
272
274
|
noFeaturedProducts: boolean;
|
|
275
|
+
/** The store's categories for the "Shop by category" rail; each opens Shop on that chip. */
|
|
276
|
+
homeCategories: StoreCategoryCard[];
|
|
277
|
+
noHomeCategories: boolean;
|
|
273
278
|
/** Schedule-only trainingFirst fields (design §4.1). They remain empty until
|
|
274
279
|
* a dated availability source can back them; collapse gates hide them. */
|
|
275
280
|
todaysClasses: ClassEntry[];
|
|
@@ -857,6 +862,7 @@ function derive(state: State): State {
|
|
|
857
862
|
noHeroImage: !hasHeroImage,
|
|
858
863
|
hasFeaturedProducts: state.featuredProducts.length > 0,
|
|
859
864
|
noFeaturedProducts: state.featuredProducts.length === 0,
|
|
865
|
+
noHomeCategories: (state.homeCategories ?? []).length === 0,
|
|
860
866
|
noSavedProducts: state.savedProducts.length === 0,
|
|
861
867
|
noSavedPosts: state.savedPosts.length === 0,
|
|
862
868
|
noSavedContent: state.savedProducts.length === 0 && state.savedPosts.length === 0,
|
|
@@ -950,6 +956,8 @@ function baseState(): State {
|
|
|
950
956
|
noHeroImage: true,
|
|
951
957
|
hasFeaturedProducts: false,
|
|
952
958
|
noFeaturedProducts: true,
|
|
959
|
+
homeCategories: [],
|
|
960
|
+
noHomeCategories: true,
|
|
953
961
|
todaysClasses: [],
|
|
954
962
|
noTodaysClasses: true,
|
|
955
963
|
coachAvatars: [],
|
|
@@ -998,6 +1006,7 @@ let refreshGen = 0;
|
|
|
998
1006
|
/** Last successful featured list — catch paths read this when the action's
|
|
999
1007
|
* `state` snapshot was captured before a racing refresh painted products. */
|
|
1000
1008
|
let lastFeaturedProducts: ProductFeature[] = [];
|
|
1009
|
+
let lastHomeCategories: StoreCategoryCard[] = [];
|
|
1001
1010
|
|
|
1002
1011
|
async function refreshPublic(state: State, arg?: unknown): Promise<State | undefined> {
|
|
1003
1012
|
// Absorb the descriptor's copy-pack overrides (if any) before any string is
|
|
@@ -1034,6 +1043,21 @@ async function refreshPublic(state: State, arg?: unknown): Promise<State | undef
|
|
|
1034
1043
|
effectiveLayout: resolveEffectiveLayout(arg, 0),
|
|
1035
1044
|
});
|
|
1036
1045
|
}
|
|
1046
|
+
// Categories ride beside the products; a failed read only hides their rail. Started
|
|
1047
|
+
// outside the try, and merged as a separate action so a slow categories endpoint cannot
|
|
1048
|
+
// hold the primary products rail behind it. Offline on a cold boot the module copy is
|
|
1049
|
+
// still empty: fall back to the rail the state restored.
|
|
1050
|
+
let categoriesResolved = false;
|
|
1051
|
+
let resolvedCategories: StoreCategoryCard[] = [];
|
|
1052
|
+
const categoriesRead = loadStoreCategories().catch(() => (lastHomeCategories.length ? lastHomeCategories : state.homeCategories ?? []));
|
|
1053
|
+
void categoriesRead.then((homeCategories) => {
|
|
1054
|
+
categoriesResolved = true;
|
|
1055
|
+
resolvedCategories = homeCategories;
|
|
1056
|
+
if (gen === refreshGen) {
|
|
1057
|
+
lastHomeCategories = homeCategories;
|
|
1058
|
+
void svc.categoriesLoaded({ generation: gen, categories: homeCategories });
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1037
1061
|
try {
|
|
1038
1062
|
const response = await queryStoreProducts(50);
|
|
1039
1063
|
if (gen !== refreshGen) return undefined;
|
|
@@ -1051,6 +1075,7 @@ async function refreshPublic(state: State, arg?: unknown): Promise<State | undef
|
|
|
1051
1075
|
welcome: welcome(),
|
|
1052
1076
|
refreshedAt: new Date().toISOString(),
|
|
1053
1077
|
featuredProducts,
|
|
1078
|
+
homeCategories: categoriesResolved ? resolvedCategories : state.homeCategories ?? [],
|
|
1054
1079
|
...bookings,
|
|
1055
1080
|
...extras,
|
|
1056
1081
|
...saved,
|
|
@@ -1075,6 +1100,7 @@ async function refreshPublic(state: State, arg?: unknown): Promise<State | undef
|
|
|
1075
1100
|
// genuine store outage still raises the banner.
|
|
1076
1101
|
isErrored: keptProducts.length === 0 && !isBridgeUnavailable(e),
|
|
1077
1102
|
featuredProducts: keptProducts,
|
|
1103
|
+
homeCategories: categoriesResolved ? resolvedCategories : state.homeCategories ?? [],
|
|
1078
1104
|
welcome: welcome(),
|
|
1079
1105
|
refreshedAt: new Date().toISOString(),
|
|
1080
1106
|
...bookings,
|
|
@@ -1087,6 +1113,7 @@ async function refreshPublic(state: State, arg?: unknown): Promise<State | undef
|
|
|
1087
1113
|
}
|
|
1088
1114
|
|
|
1089
1115
|
let personalGeneration = 0;
|
|
1116
|
+
let shopCategoryActivation = 0;
|
|
1090
1117
|
function memberSchedule(rows: MemberBookingRow[], now = new Date()): { label: string; fraction: number } {
|
|
1091
1118
|
const weekStart = new Date(now);
|
|
1092
1119
|
const day = (weekStart.getDay() + 6) % 7;
|
|
@@ -1184,20 +1211,33 @@ const svc = defineService<State, {
|
|
|
1184
1211
|
refresh: (state: State, arg?: unknown) => Promise<State | undefined>;
|
|
1185
1212
|
dismissOnboarding: (state: State) => State;
|
|
1186
1213
|
clearPersonal: (state: State) => State;
|
|
1214
|
+
openShopCategory: (state: State, arg?: unknown) => State;
|
|
1187
1215
|
personalLoaded: (state: State, arg: { generation: number; rows: MemberBookingRow[]; progress: { label: string; fraction: number }; error: boolean; weekly: { count: number; fraction: number; label: string } | null }) => State;
|
|
1216
|
+
categoriesLoaded: (state: State, arg: { generation: number; categories: StoreCategoryCard[] }) => State | undefined;
|
|
1188
1217
|
}>("himiBrandedHome", {
|
|
1189
1218
|
state: baseState(),
|
|
1190
1219
|
// The host's site is read fresh at every boot: a snapshot of the last session's site must not
|
|
1191
1220
|
// decide this one's verticals or name (an owner installs or removes one; another site opens).
|
|
1192
|
-
notHydrated: ["hosted", "hostedName", "hostedCards"],
|
|
1221
|
+
notHydrated: ["hosted", "hostedName", "hostedCards", "homeCategories", "noHomeCategories"],
|
|
1193
1222
|
actions: {
|
|
1194
1223
|
clearPersonal: state => derive({ ...state, nextBooking: [], hideNextBooking: true, memberProgressLabel: "", memberProgressFraction: 0, hideMemberProgress: true, personalError: false, hidePersonalError: true,
|
|
1195
1224
|
weeklyClassCount: 0, weeklyClassGoalFraction: 0, weeklyGoalLabel: "" }),
|
|
1225
|
+
openShopCategory: (state, arg) => {
|
|
1226
|
+
const category = String((arg as { category?: unknown } | undefined)?.category ?? "");
|
|
1227
|
+
if (!category) return state;
|
|
1228
|
+
shopCategoryActivation++;
|
|
1229
|
+
// Nav params are strings and the activation nonce is a transient route event,
|
|
1230
|
+
// not a distinct Shop record. ScreenHost forwards these live params to onAppear
|
|
1231
|
+
// when a pooled worker is reused.
|
|
1232
|
+
const activation = `${Date.now()}-${shopCategoryActivation}`;
|
|
1233
|
+
return navigate(state, { screenId: "himi_shop", args: { category, categoryId: category, categoryActivation: activation } });
|
|
1234
|
+
},
|
|
1196
1235
|
personalLoaded: (state, arg) => arg.generation !== personalGeneration ? state : derive({ ...state,
|
|
1197
1236
|
nextBooking: arg.rows, hideNextBooking: !arg.rows.length,
|
|
1198
1237
|
memberProgressLabel: arg.progress.label, memberProgressFraction: arg.progress.fraction, hideMemberProgress: !arg.progress.label,
|
|
1199
1238
|
personalError: arg.error, hidePersonalError: !arg.error,
|
|
1200
1239
|
weeklyClassCount: arg.weekly?.count ?? 0, weeklyClassGoalFraction: arg.weekly?.fraction ?? 0, weeklyGoalLabel: arg.weekly?.label ?? "" }),
|
|
1240
|
+
categoriesLoaded: (state, arg) => arg.generation !== refreshGen ? undefined : derive({ ...state, homeCategories: arg.categories }),
|
|
1201
1241
|
refresh,
|
|
1202
1242
|
// "Got it" on the first-run card: persist + hide. Persistence is
|
|
1203
1243
|
// best-effort — the UI hides either way for this session.
|
|
@@ -1213,7 +1253,7 @@ const svc = defineService<State, {
|
|
|
1213
1253
|
// name. In place, because on those runtimes getState() IS the stored state (as himi-player-home does).
|
|
1214
1254
|
if ((globalThis as { himalaya?: { hydrationOptOut?: unknown } }).himalaya?.hydrationOptOut !== true) {
|
|
1215
1255
|
const live = (svc as { getState?: () => State | undefined }).getState?.();
|
|
1216
|
-
if (live && typeof live === "object") Object.assign(live, hostedBoot);
|
|
1256
|
+
if (live && typeof live === "object") Object.assign(live, hostedBoot, { homeCategories: [], noHomeCategories: true });
|
|
1217
1257
|
}
|
|
1218
1258
|
|
|
1219
1259
|
svc.refresh();
|