@wix/himalaya-cli 0.1525.0 → 0.1527.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 +14 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/state/src/connectivity.d.ts +5 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/state/src/index.d.ts +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +22 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/worker-subtree-safe-types.generated.d.ts +4 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +7 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +13 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/state.mjs +14 -0
- package/dist/cli.mjs +157 -40
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/designing-himi-screens.md +43 -18
- package/guides/himi-test.md +3 -3
- package/guides/recipe-dynamic-list.md +7 -7
- package/guides/recipe-four-state.md +47 -41
- package/guides/recipe-search.md +1 -1
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +1943 -646
- package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/beauty-salon/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/beauty-salon/tier5-src/himi-booking-bookings/index.ts +1 -0
- package/templates/beauty-salon/tier5-src/himi-booking-time/index.ts +803 -284
- package/templates/beauty-salon/tier5-src/himi-branded-lite-address/index.ts +3 -1
- package/templates/beauty-salon/tier5-src/himi-branded-lite-addresses/index.ts +3 -1
- package/templates/beauty-salon/tier5-src/himi-branded-lite-agenda/index.ts +9 -1
- package/templates/beauty-salon/tier5-src/himi-branded-lite-cart/index.ts +3 -0
- package/templates/beauty-salon/tier5-src/himi-branded-lite-order/index.ts +3 -1
- package/templates/beauty-salon/tier5-src/himi-branded-lite-place-summary/index.ts +12 -1
- package/templates/beauty-salon/tier5-src/himi-branded-lite-plans/index.ts +13 -1
- package/templates/beauty-salon/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
- package/templates/beauty-salon/tier5-src/himi-store-account/index.ts +69 -10
- package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +10 -4
- package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/beauty-salon-owner/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/branded-lite/dev/index.ts +2404 -1020
- package/templates/branded-lite/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/branded-lite/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/branded-lite/tier5-src/business-chat/index.ts +4 -0
- package/templates/branded-lite/tier5-src/himi-booking-bookings/index.ts +1 -0
- package/templates/branded-lite/tier5-src/himi-booking-time/index.ts +803 -284
- package/templates/branded-lite/tier5-src/himi-branded-lite-address/index.ts +3 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-addresses/index.ts +3 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-agenda/index.ts +9 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-cart/index.ts +3 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-feed/index.ts +3 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-group/index.ts +3 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-notifications/index.ts +6 -4
- package/templates/branded-lite/tier5-src/himi-branded-lite-notifsettings/index.ts +6 -4
- package/templates/branded-lite/tier5-src/himi-branded-lite-order/index.ts +3 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-place-summary/index.ts +12 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-plans/index.ts +13 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +3 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-reservations/index.ts +6 -4
- package/templates/branded-lite/tier5-src/himi-branded-lite-reserve/index.ts +3 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
- package/templates/branded-lite/tier5-src/himi-events-detail/index.ts +8 -1
- package/templates/branded-lite/tier5-src/himi-events-rsvp/index.ts +7 -4
- package/templates/branded-lite/tier5-src/himi-store-account/index.ts +69 -10
- package/templates/branded-lite/tier5-src/himi-store-product/index.ts +10 -4
- package/templates/community-club/dev/index.ts +40 -21
- package/templates/community-club/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/community-club/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/community-club/tier5-src/himi-branded-lite-feed/index.ts +3 -1
- package/templates/community-club/tier5-src/himi-branded-lite-group/index.ts +3 -1
- package/templates/community-club/tier5-src/himi-events-detail/index.ts +8 -1
- package/templates/community-club/tier5-src/himi-events-rsvp/index.ts +7 -4
- package/templates/community-club/tier5-src/himi-store-account/index.ts +69 -10
- package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/community-club-owner/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/events-meetups/dev/index.ts +36 -19
- package/templates/events-meetups/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/events-meetups/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/events-meetups/tier5-src/himi-branded-lite-feed/index.ts +3 -1
- package/templates/events-meetups/tier5-src/himi-branded-lite-group/index.ts +3 -1
- package/templates/events-meetups/tier5-src/himi-events-detail/index.ts +8 -1
- package/templates/events-meetups/tier5-src/himi-events-rsvp/index.ts +7 -4
- package/templates/events-meetups/tier5-src/himi-store-account/index.ts +69 -10
- package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/events-meetups-owner/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/gym-fitness/dev/index.ts +1944 -645
- package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/gym-fitness/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/gym-fitness/tier5-src/himi-booking-bookings/index.ts +1 -0
- package/templates/gym-fitness/tier5-src/himi-booking-time/index.ts +803 -284
- package/templates/gym-fitness/tier5-src/himi-branded-lite-address/index.ts +3 -1
- package/templates/gym-fitness/tier5-src/himi-branded-lite-addresses/index.ts +3 -1
- package/templates/gym-fitness/tier5-src/himi-branded-lite-agenda/index.ts +9 -1
- package/templates/gym-fitness/tier5-src/himi-branded-lite-cart/index.ts +3 -0
- package/templates/gym-fitness/tier5-src/himi-branded-lite-order/index.ts +3 -1
- package/templates/gym-fitness/tier5-src/himi-branded-lite-place-summary/index.ts +12 -1
- package/templates/gym-fitness/tier5-src/himi-branded-lite-plans/index.ts +13 -1
- package/templates/gym-fitness/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
- package/templates/gym-fitness/tier5-src/himi-events-detail/index.ts +8 -1
- package/templates/gym-fitness/tier5-src/himi-events-rsvp/index.ts +7 -4
- package/templates/gym-fitness/tier5-src/himi-store-account/index.ts +69 -10
- package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +10 -4
- package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/gym-fitness-owner/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/nonprofit-cause/dev/index.ts +47 -29
- package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/nonprofit-cause/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-feed/index.ts +3 -1
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-group/index.ts +3 -1
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-place-summary/index.ts +12 -1
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-plans/index.ts +13 -1
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
- package/templates/nonprofit-cause/tier5-src/himi-events-detail/index.ts +8 -1
- package/templates/nonprofit-cause/tier5-src/himi-events-rsvp/index.ts +7 -4
- package/templates/nonprofit-cause/tier5-src/himi-store-account/index.ts +69 -10
- package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/nonprofit-cause-owner/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/owner-lite/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/owner-lite/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/podcast-creator/dev/index.ts +47 -29
- package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/podcast-creator/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/podcast-creator/tier5-src/himi-branded-lite-feed/index.ts +3 -1
- package/templates/podcast-creator/tier5-src/himi-branded-lite-place-summary/index.ts +12 -1
- package/templates/podcast-creator/tier5-src/himi-branded-lite-plans/index.ts +13 -1
- package/templates/podcast-creator/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
- package/templates/podcast-creator/tier5-src/himi-events-detail/index.ts +8 -1
- package/templates/podcast-creator/tier5-src/himi-events-rsvp/index.ts +7 -4
- package/templates/podcast-creator/tier5-src/himi-store-account/index.ts +69 -10
- package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/podcast-creator-owner/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/portfolio-creative/dev/index.ts +52 -27
- package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/portfolio-creative/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-address/index.ts +3 -1
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-addresses/index.ts +3 -1
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-cart/index.ts +3 -0
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-order/index.ts +3 -1
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
- package/templates/portfolio-creative/tier5-src/himi-events-detail/index.ts +8 -1
- package/templates/portfolio-creative/tier5-src/himi-events-rsvp/index.ts +7 -4
- package/templates/portfolio-creative/tier5-src/himi-store-account/index.ts +69 -10
- package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +10 -4
- package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/portfolio-creative-owner/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/restaurant/dev/index.ts +48 -25
- package/templates/restaurant/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/restaurant/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/restaurant/tier5-src/himi-branded-lite-address/index.ts +3 -1
- package/templates/restaurant/tier5-src/himi-branded-lite-addresses/index.ts +3 -1
- package/templates/restaurant/tier5-src/himi-branded-lite-reserve/index.ts +3 -1
- package/templates/restaurant/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
- package/templates/restaurant/tier5-src/himi-events-detail/index.ts +8 -1
- package/templates/restaurant/tier5-src/himi-events-rsvp/index.ts +7 -4
- package/templates/restaurant/tier5-src/himi-store-account/index.ts +69 -10
- package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/restaurant-owner/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/retail-store/dev/index.ts +44 -23
- package/templates/retail-store/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/retail-store/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/retail-store/tier5-src/himi-branded-lite-address/index.ts +3 -1
- package/templates/retail-store/tier5-src/himi-branded-lite-addresses/index.ts +3 -1
- package/templates/retail-store/tier5-src/himi-branded-lite-cart/index.ts +3 -0
- package/templates/retail-store/tier5-src/himi-branded-lite-order/index.ts +3 -1
- package/templates/retail-store/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
- package/templates/retail-store/tier5-src/himi-store-account/index.ts +69 -10
- package/templates/retail-store/tier5-src/himi-store-product/index.ts +10 -4
- package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/retail-store-owner/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/yoga-studio/dev/index.ts +2102 -811
- package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/yoga-studio/tier5-src/_shared/online-actions.ts +76 -0
- package/templates/yoga-studio/tier5-src/himi-booking-bookings/index.ts +1 -0
- package/templates/yoga-studio/tier5-src/himi-booking-time/index.ts +803 -284
- package/templates/yoga-studio/tier5-src/himi-branded-lite-agenda/index.ts +9 -1
- package/templates/yoga-studio/tier5-src/himi-branded-lite-place-summary/index.ts +12 -1
- package/templates/yoga-studio/tier5-src/himi-branded-lite-plans/index.ts +13 -1
- package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +3 -1
- package/templates/yoga-studio/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
- package/templates/yoga-studio/tier5-src/himi-events-detail/index.ts +8 -1
- package/templates/yoga-studio/tier5-src/himi-events-rsvp/index.ts +7 -4
- package/templates/yoga-studio/tier5-src/himi-store-account/index.ts +69 -10
- package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +5 -1
- package/templates/yoga-studio-owner/tier5-src/_shared/online-actions.ts +76 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { withOnlineWrites, offlineWritesEnabled } from "../_shared/online-actions.js";
|
|
1
2
|
// Tier-5 bundle: himi-events — member RSVP flow.
|
|
2
|
-
import { defineService } from "@himalaya/state";
|
|
3
|
+
import { defineService as registerService } from "@himalaya/state";
|
|
3
4
|
import { dispatch } from "@himalaya/dispatch";
|
|
4
5
|
import { createRsvp, getEvent, type WixEvent } from "@himalaya/wix-events";
|
|
5
6
|
import { displayName, getMyData, type WixMyData } from "@himalaya/wix-users";
|
|
@@ -125,6 +126,7 @@ function contactName(dataLine: string): { first: string; last: string } {
|
|
|
125
126
|
return { first: parts[0], last: parts.slice(1).join(" ") };
|
|
126
127
|
}
|
|
127
128
|
|
|
129
|
+
const defineService = withOnlineWrites(registerService, ["rsvp", "signIn"], { derive, secretFields: ["password"] });
|
|
128
130
|
const svc = defineService<S, {
|
|
129
131
|
load: (s: S, arg?: unknown) => Promise<S>;
|
|
130
132
|
setEmail: (s: S, arg?: unknown) => S;
|
|
@@ -202,16 +204,17 @@ const svc = defineService<S, {
|
|
|
202
204
|
// Optimistic local write — the RSVP row lands in the cache before the Wix POST.
|
|
203
205
|
const rsvpRow = (id: string): DbEntity => ({ id, eventId: s.id, firstName: name.first, lastName: name.last, email: guestEmail, status: "YES", title: s.title });
|
|
204
206
|
const localId = s.id;
|
|
205
|
-
await cachePut(rsvpRow(localId));
|
|
207
|
+
if (!offlineWritesEnabled) await cachePut(rsvpRow(localId));
|
|
206
208
|
try {
|
|
207
209
|
const res = await createRsvp(s.id, name.first, name.last, guestEmail, "YES");
|
|
208
210
|
// Reconcile the optimistic local id with the server-assigned rsvp id.
|
|
209
211
|
const serverId = res.rsvp?.id;
|
|
210
|
-
if (
|
|
212
|
+
if (offlineWritesEnabled) await cachePut(rsvpRow(serverId || localId));
|
|
213
|
+
else if (serverId && serverId !== localId) { await cacheDrop(localId); await cachePut(rsvpRow(serverId)); }
|
|
211
214
|
rememberRsvp(s.id);
|
|
212
215
|
return derive({ ...s, successMessage: copyParam("rsvp.successMessage", "You are going. Added to My Events."), message: "" });
|
|
213
216
|
} catch (e) {
|
|
214
|
-
if (isBridgeUnavailable(e)) {
|
|
217
|
+
if (!offlineWritesEnabled && isBridgeUnavailable(e)) {
|
|
215
218
|
// Offline / no bridge: the optimistic cache row already shows; queue the
|
|
216
219
|
// write to replay (WriteQueue, idempotent) when connectivity returns.
|
|
217
220
|
await queueWrite("tier5.himiEventsRsvp.rsvp", { id: s.id, firstName: name.first, lastName: name.last, email: guestEmail, status: "YES" });
|
|
@@ -26,6 +26,10 @@ interface AuthRouteInput {
|
|
|
26
26
|
returnScreenId?: string;
|
|
27
27
|
returnId?: string;
|
|
28
28
|
returnChannelId?: string;
|
|
29
|
+
/** Standalone sign-in only: "1" when it was opened as a modal (sheet / cover). */
|
|
30
|
+
modal?: string;
|
|
31
|
+
/** Modal only: the tab whose stack the return screen opens on once the modal closes. */
|
|
32
|
+
returnTab?: string;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
// The screen's boot input survives refresh; consuming it must survive too.
|
|
@@ -61,6 +65,12 @@ function authIntentFromArg(arg: unknown, prev: Pick<S, "authIntent" | "authConte
|
|
|
61
65
|
};
|
|
62
66
|
}
|
|
63
67
|
|
|
68
|
+
/** How the standalone form leaves. A modal is closed: on web `back` pops the tab's stack,
|
|
69
|
+
* not the modal. A pushed form goes back. */
|
|
70
|
+
function leaveNav(): "dismiss" | "back" {
|
|
71
|
+
return routeInput().modal === "1" ? "dismiss" : "back";
|
|
72
|
+
}
|
|
73
|
+
|
|
64
74
|
/** Where a successful sign-in returns to, or null. Never Account itself — that would
|
|
65
75
|
* only push a second copy of the screen the member is already on. */
|
|
66
76
|
function resumeTarget(s: Pick<S, "returnScreenId" | "returnId" | "returnChannelId">): { screenId: string; args?: Record<string, string> } | null {
|
|
@@ -78,11 +88,26 @@ function resumeTarget(s: Pick<S, "returnScreenId" | "returnId" | "returnChannelI
|
|
|
78
88
|
* On the standalone sign-in screen the target is pushed, not a `replace`: on iOS and
|
|
79
89
|
* Android `replace` resets the stack to its root, which would drop the screens the
|
|
80
90
|
* visitor came through. Back from the target shows the form again, and the form sends a
|
|
81
|
-
* member back on appear (settle). With no target the form goes back.
|
|
91
|
+
* member back on appear (settle). With no target the form goes back.
|
|
92
|
+
*
|
|
93
|
+
* A modal form closes. A target pushed from here would open inside the modal (iOS, Android)
|
|
94
|
+
* or under it (web), so a modal that names a `returnTab` hands the target to that tab: the
|
|
95
|
+
* host closes the modal, then pushes it on that tab's stack (Account's member rows). A host
|
|
96
|
+
* showing no such tab (a drawer, a narrow Mac window) drops that request whole, modal and
|
|
97
|
+
* all, so the modal is also dismissed directly: it always closes, and the row opens where
|
|
98
|
+
* the tab exists. With no `returnTab` it only closes, and the screen under it loads again —
|
|
99
|
+
* the screen that opened the modal is the return target. */
|
|
82
100
|
function resumeAfterAuth(next: S): S {
|
|
83
101
|
const target = resumeTarget(next);
|
|
84
102
|
if (!next.isMember) return next;
|
|
85
|
-
|
|
103
|
+
const tab = routeInput().modal === "1" ? routeInput().returnTab ?? "" : "";
|
|
104
|
+
if (target && tab && next.standalone) {
|
|
105
|
+
routeInputConsumed = true;
|
|
106
|
+
void dispatch("himi.open", { presentation: "dismiss" });
|
|
107
|
+
const cleared = derive({ ...next, authIntent: "", authContextName: "", returnScreenId: "", returnId: "", returnChannelId: "" });
|
|
108
|
+
return navigate(cleared, target.args ? { tab, screenId: target.screenId, args: target.args } : { tab, screenId: target.screenId });
|
|
109
|
+
}
|
|
110
|
+
if (!target || routeInput().modal === "1") return next.standalone ? navigate(next, { presentation: leaveNav() }) : next;
|
|
86
111
|
routeInputConsumed = true;
|
|
87
112
|
const cleared = derive({ ...next, authIntent: "", authContextName: "", returnScreenId: "", returnId: "", returnChannelId: "" });
|
|
88
113
|
return navigate(cleared, target);
|
|
@@ -96,11 +121,31 @@ function forwardToSignIn(next: S): S {
|
|
|
96
121
|
if (!next.authIntent && !next.returnScreenId) return next;
|
|
97
122
|
routeInputConsumed = true;
|
|
98
123
|
const args: Record<string, string> = { mode: "signin" };
|
|
99
|
-
for (const key of ["authIntent", "authContextName"
|
|
100
|
-
if (next[key]) args[key === "authIntent" ? "intent" :
|
|
124
|
+
for (const key of ["authIntent", "authContextName"] as const) {
|
|
125
|
+
if (next[key]) args[key === "authIntent" ? "intent" : "contextName"] = next[key];
|
|
126
|
+
}
|
|
127
|
+
return openSignIn(derive({ ...next, authIntent: "", authContextName: "", returnScreenId: "", returnId: "", returnChannelId: "" }),
|
|
128
|
+
args, resumeTarget(next));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Account sends a guest to the standalone sign-in screen, with `after`, the member screen to
|
|
132
|
+
* open once signed in. With a `signInPresentation` appear param it opens as that modal, and
|
|
133
|
+
* `after` goes on Account's own tab (`signInReturnTab`) when the modal closes; pushed, the form
|
|
134
|
+
* pushes `after` itself (resumeAfterAuth). */
|
|
135
|
+
function openSignIn(next: S, args: Record<string, string>, after: { screenId: string; args?: Record<string, string> } | null): S {
|
|
136
|
+
const screenId = next.signInScreen || "himi_sign_in";
|
|
137
|
+
const presentation = signInPresentationRequested;
|
|
138
|
+
const back: Record<string, string> = {};
|
|
139
|
+
if (after) {
|
|
140
|
+
back.returnScreenId = after.screenId;
|
|
141
|
+
if (after.args?.id) back.returnId = after.args.id;
|
|
142
|
+
if (after.args?.channelId) back.returnChannelId = after.args.channelId;
|
|
101
143
|
}
|
|
102
|
-
return navigate(
|
|
103
|
-
|
|
144
|
+
if (!presentation) return navigate(next, { screenId, args: { ...args, ...back } });
|
|
145
|
+
const modalArgs: Record<string, string> = { ...args, modal: "1" };
|
|
146
|
+
// With no tab to open it on, a modal has no way to the member screen: it only closes.
|
|
147
|
+
if (after && signInReturnTabRequested) Object.assign(modalArgs, back, { returnTab: signInReturnTabRequested });
|
|
148
|
+
return navigate(next, { screenId, presentation, args: modalArgs });
|
|
104
149
|
}
|
|
105
150
|
|
|
106
151
|
/** After a load: the standalone sign-in screen sends a member back (a stale link, or a
|
|
@@ -128,8 +173,11 @@ function settle(next: S, arg: unknown, latest = true): S {
|
|
|
128
173
|
// The appear refresh returns `_nav`, which every host applies — binaries from before
|
|
129
174
|
// worker nav dispatch included. Only an arg-less load, whose result iOS
|
|
130
175
|
// discards, dispatches.
|
|
131
|
-
if (strField(arg, "standalone") === "1") return navigate(cleared, { presentation:
|
|
132
|
-
|
|
176
|
+
if (strField(arg, "standalone") === "1") return navigate(cleared, { presentation: leaveNav() });
|
|
177
|
+
// Not from a modal: nothing pushed over it comes back to it, and on iOS the closing modal's
|
|
178
|
+
// own screen reloads here once, after it has asked to close — a late dismiss would close
|
|
179
|
+
// whatever modal is open by then.
|
|
180
|
+
if (routeInput().modal !== "1") void dispatch("himi.open", { presentation: leaveNav() });
|
|
133
181
|
return cleared;
|
|
134
182
|
}
|
|
135
183
|
// Account forwards only on an appear refresh: the boot `load()` has no params and its
|
|
@@ -180,6 +228,10 @@ let appliedSeq = 0;
|
|
|
180
228
|
// The appear params belong to the worker session too, like the two flags above: a probe
|
|
181
229
|
// dropped because the visitor typed must not take them with it.
|
|
182
230
|
let signInScreenRequested = "";
|
|
231
|
+
/** Account only: how it opens the standalone sign-in screen ("" = push). */
|
|
232
|
+
let signInPresentationRequested: "" | "sheet" | "full_screen" = "";
|
|
233
|
+
/** Account only: its tab, where a member screen opens after a modal sign-in. */
|
|
234
|
+
let signInReturnTabRequested = "";
|
|
183
235
|
let standaloneRequested = false;
|
|
184
236
|
let pollIdentityRequested = false;
|
|
185
237
|
// Bumped by every mode switch, so a dropped probe never puts back a mode the visitor left.
|
|
@@ -210,6 +262,9 @@ function appearConfig(s: S, arg: unknown): AppearConfig {
|
|
|
210
262
|
passwordDraft = "";
|
|
211
263
|
}
|
|
212
264
|
signInScreenRequested ||= strField(arg, "signInScreen") || s.signInScreen;
|
|
265
|
+
const presentation = strField(arg, "signInPresentation");
|
|
266
|
+
if (presentation === "sheet" || presentation === "full_screen") signInPresentationRequested = presentation;
|
|
267
|
+
signInReturnTabRequested ||= strField(arg, "signInReturnTab");
|
|
213
268
|
// Only the sign-in screen's route carries `mode`, so it marks the form from the boot load on.
|
|
214
269
|
standaloneRequested ||= s.standalone || appearing || (!routeInputConsumed && !!routeInput().mode);
|
|
215
270
|
pollIdentityRequested ||= s.pollIdentity || strField(arg, "pollIdentity") === "1";
|
|
@@ -676,6 +731,7 @@ const svc = defineService<S, {
|
|
|
676
731
|
signOut: (s: S) => Promise<S>;
|
|
677
732
|
sync: (s: S) => Promise<S | undefined>;
|
|
678
733
|
openMember: (s: S, arg?: unknown) => Promise<S>;
|
|
734
|
+
leave: (s: S) => S;
|
|
679
735
|
restorePublish: (s: S) => S;
|
|
680
736
|
iapRestore: (s: S) => Promise<undefined>;
|
|
681
737
|
}>("himiStoreAccount", {
|
|
@@ -756,7 +812,8 @@ const svc = defineService<S, {
|
|
|
756
812
|
return "next" in r ? r.next : undefined;
|
|
757
813
|
},
|
|
758
814
|
// A row to a member-only screen shows to a guest too. A member opens it; a guest signs
|
|
759
|
-
// in first and the sign-in screen
|
|
815
|
+
// in first, and the sign-in screen then opens the row's screen — as a modal, by closing
|
|
816
|
+
// and pushing it on this tab (openSignIn) — so Back returns here either way.
|
|
760
817
|
openMember: async (s, arg): Promise<S> => {
|
|
761
818
|
const screenId = strField(arg, "screenId");
|
|
762
819
|
if (!screenId) return s;
|
|
@@ -764,8 +821,10 @@ const svc = defineService<S, {
|
|
|
764
821
|
// From the state as it is now: a probe may have landed during the identity read.
|
|
765
822
|
const now = svc.getState();
|
|
766
823
|
if (member) return navigate(now, { screenId });
|
|
767
|
-
return
|
|
824
|
+
return openSignIn(now, { mode: "signin" }, { screenId });
|
|
768
825
|
},
|
|
826
|
+
// The standalone form's Continue: leaves the form the way it was opened.
|
|
827
|
+
leave: (s) => navigate(s, { presentation: leaveNav() }),
|
|
769
828
|
// Typing means any focus request was delivered: clear the tokens (safe while mounted, since
|
|
770
829
|
// bumpToken never repeats), so a visit rarely ends — and a pooled form rarely reopens —
|
|
771
830
|
// holding one.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withOnlineWrites } from './online-actions.js';
|
|
1
2
|
import { defineService as register, type ServiceDefinition } from '@himalaya/state';
|
|
2
3
|
import { COLLECTIONS, COLLECTION_CHROME_MIN_ITEMS, initialCollection, normalizeCollection, projectCollection, collectionId, collectionActionsLabel, type CollectionRow } from './collection-contract.js';
|
|
3
4
|
import { localPreferences, type PreferenceRuntime } from './local-preferences.js';
|
|
@@ -331,6 +332,8 @@ export function decorateCollection(name: string, def: { state: State; actions?:
|
|
|
331
332
|
* reducer and reads the private controller snapshot, never an SDUI payload. */
|
|
332
333
|
export function defineBackendCollection<S extends object, Row extends object>(name: string, options: {
|
|
333
334
|
register?: typeof register;
|
|
335
|
+
/** Explicit mutation preflight; unrelated collection services keep their current reducers. */
|
|
336
|
+
onlineWrites?: boolean;
|
|
334
337
|
/** Revalidate already opened pages on navigation return; explicit refresh resets. */
|
|
335
338
|
restorePagesOnLoad?: boolean;
|
|
336
339
|
/** The load action's argument (onAppear params beyond `copy`), before the query runs — for a
|
|
@@ -350,7 +353,8 @@ export function defineBackendCollection<S extends object, Row extends object>(na
|
|
|
350
353
|
controller: controller as unknown as BackendCollection['controller'],
|
|
351
354
|
apply: (state, page) => options.apply(state as S, page as unknown as CollectionQueryState<Row>) as State,
|
|
352
355
|
});
|
|
353
|
-
const
|
|
356
|
+
const registerService = options.onlineWrites ? withOnlineWrites(options.register ?? register, ['confirmCancel', 'confirmOperation', 'retryOperation', 'commitWrite'], { messageFields: ['actionMessage'] }) : (options.register ?? register);
|
|
357
|
+
const handle = registerService(name, definition);
|
|
354
358
|
// Registration restored the state a cold boot saved: the page it showed is this run's, before the first load. Only
|
|
355
359
|
// when the service's own rows came back with it: a bundle that keeps them out of the restore (its `notHydrated`)
|
|
356
360
|
// starts its first load as before, never from rows it chose not to restore. Rows that came back empty are the
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { connectivity, defineService as register, type ServiceDefinition } from '@himalaya/state';
|
|
2
|
+
|
|
3
|
+
// Template generation enables this behavior only in the selected two starter families.
|
|
4
|
+
export const offlineWritesEnabled = false;
|
|
5
|
+
|
|
6
|
+
export const offlineWriteMessage = 'You’re offline. Connect to save this change, then submit it again.';
|
|
7
|
+
|
|
8
|
+
/** The guard runs before the reducer: entered values survive and no optimistic
|
|
9
|
+
* changes, success state, or queued writes can occur. It never submits on reconnect. */
|
|
10
|
+
export function offlineWriteState<S extends object>(state: S): S {
|
|
11
|
+
const next: Record<string, unknown> = { ...state, error: offlineWriteMessage, message: offlineWriteMessage };
|
|
12
|
+
for (const key of ['saving', 'isSaving', 'submitting', 'isSubmitting', 'adding', 'added', 'bookingInFlight']) {
|
|
13
|
+
if (key in next) next[key] = false;
|
|
14
|
+
}
|
|
15
|
+
for (const key of ['savedMessage', 'successMessage']) {
|
|
16
|
+
if (key in next) next[key] = '';
|
|
17
|
+
}
|
|
18
|
+
// A saved/favourited item is business state, not a submission-success flag.
|
|
19
|
+
return next as S;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface OfflineWriteOptions<S> {
|
|
23
|
+
derive?: (state: S) => S;
|
|
24
|
+
/** Exempt only non-mutating branches of a registered action. */
|
|
25
|
+
requiresConnectivity?: (action: string, state: S, argument?: unknown) => boolean;
|
|
26
|
+
messageFields?: readonly string[] | ((action: string) => readonly string[]);
|
|
27
|
+
draftFields?: readonly string[] | Readonly<Record<string, string>>;
|
|
28
|
+
secretFields?: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function withOnlineWrites<S extends object>(registerService: typeof register, names: readonly string[], options: OfflineWriteOptions<S> = {}): typeof register {
|
|
32
|
+
if (!offlineWritesEnabled) return registerService;
|
|
33
|
+
return ((name: string, definition: ServiceDefinition<Record<string, unknown>>) => {
|
|
34
|
+
let currentState: (() => Record<string, unknown>) | undefined;
|
|
35
|
+
const handle = registerService(name, {
|
|
36
|
+
...definition,
|
|
37
|
+
notHydrated: [...new Set([...(definition.notHydrated ?? []), ...(options.secretFields ?? [])])],
|
|
38
|
+
actions: Object.fromEntries(Object.entries(definition.actions ?? {}).map(([action, reduce]) => [action,
|
|
39
|
+
(state: Record<string, unknown>, ...args: never[]) => {
|
|
40
|
+
if (!names.includes(action)) return reduce(state, ...args);
|
|
41
|
+
if (options.requiresConnectivity?.(action, state as S, args[0]) === false) return reduce(state, ...args);
|
|
42
|
+
const fields = typeof options.messageFields === 'function' ? options.messageFields(action) : options.messageFields ?? ['message'];
|
|
43
|
+
if (connectivity().online) {
|
|
44
|
+
let cleared = false;
|
|
45
|
+
for (const field of ['error', 'message', ...fields]) {
|
|
46
|
+
if (state[field] === offlineWriteMessage) { state[field] = ''; cleared = true; }
|
|
47
|
+
}
|
|
48
|
+
// Keep the runtime's state object: reducers may mutate it and return undefined.
|
|
49
|
+
const result = reduce(state, ...args);
|
|
50
|
+
if (!cleared) return result;
|
|
51
|
+
// Publish cleared feedback even for a void reducer. An async completion must
|
|
52
|
+
// keep any newer state published by another action while it was awaiting.
|
|
53
|
+
const finish = (next: Record<string, unknown> | undefined) => next === undefined ? currentState?.() ?? state : next;
|
|
54
|
+
return result && typeof (result as Promise<unknown>).then === 'function'
|
|
55
|
+
? Promise.resolve(result).then(finish)
|
|
56
|
+
: finish(result as Record<string, unknown> | undefined);
|
|
57
|
+
}
|
|
58
|
+
const draft = { ...state };
|
|
59
|
+
const argument = args[0] as Record<string, unknown> | undefined;
|
|
60
|
+
const draftFields = options.draftFields ?? [];
|
|
61
|
+
const entries = Array.isArray(draftFields) ? draftFields.map(key => [key, key]) : Object.entries(draftFields);
|
|
62
|
+
for (const [key, source] of entries) {
|
|
63
|
+
if (argument && key in state && typeof argument[source] === typeof state[key]) draft[key] = argument[source];
|
|
64
|
+
}
|
|
65
|
+
for (const key of options.secretFields ?? []) draft[key] = '';
|
|
66
|
+
const feedback = Object.fromEntries(fields.map(key => [key, offlineWriteMessage]));
|
|
67
|
+
let next = { ...offlineWriteState(draft), ...feedback };
|
|
68
|
+
if (options.derive) next = options.derive(next as S) as Record<string, unknown>;
|
|
69
|
+
return { ...next, ...feedback };
|
|
70
|
+
},
|
|
71
|
+
])),
|
|
72
|
+
});
|
|
73
|
+
if (typeof handle.getState === 'function') currentState = () => handle.getState();
|
|
74
|
+
return handle;
|
|
75
|
+
}) as typeof register;
|
|
76
|
+
}
|
|
@@ -2173,8 +2173,10 @@ const SCREENS = runtimeSetting({
|
|
|
2173
2173
|
"actionId": "himi.open",
|
|
2174
2174
|
"params": {
|
|
2175
2175
|
"screenId": "himi_sign_in",
|
|
2176
|
-
"mode": "signin"
|
|
2177
|
-
|
|
2176
|
+
"mode": "signin",
|
|
2177
|
+
"modal": "1"
|
|
2178
|
+
},
|
|
2179
|
+
"presentation": "sheet"
|
|
2178
2180
|
}
|
|
2179
2181
|
}
|
|
2180
2182
|
}
|
|
@@ -2192,8 +2194,10 @@ const SCREENS = runtimeSetting({
|
|
|
2192
2194
|
"actionId": "himi.open",
|
|
2193
2195
|
"params": {
|
|
2194
2196
|
"screenId": "himi_sign_in",
|
|
2195
|
-
"mode": "register"
|
|
2196
|
-
|
|
2197
|
+
"mode": "register",
|
|
2198
|
+
"modal": "1"
|
|
2199
|
+
},
|
|
2200
|
+
"presentation": "sheet"
|
|
2197
2201
|
}
|
|
2198
2202
|
}
|
|
2199
2203
|
}
|
|
@@ -3395,6 +3399,8 @@ const SCREENS = runtimeSetting({
|
|
|
3395
3399
|
"memberAccountLinks": "1",
|
|
3396
3400
|
"restorePurchases": "1",
|
|
3397
3401
|
"signInScreen": "himi_sign_in",
|
|
3402
|
+
"signInPresentation": "sheet",
|
|
3403
|
+
"signInReturnTab": "profile",
|
|
3398
3404
|
"pollIdentity": "1",
|
|
3399
3405
|
"cartCount": "1",
|
|
3400
3406
|
"copy": "{\"account.subtitle\":\"Orders, RSVPs and saved content in one place.\",\"storeAccount.authBody\":\"Sign in to continue — cart and RSVPs stay with you.\",\"storeAccount.authBodyRegister\":\"Create a member account to continue — cart and RSVPs stay with you.\"}"
|
|
@@ -3670,8 +3676,10 @@ const SCREENS = runtimeSetting({
|
|
|
3670
3676
|
"params": {
|
|
3671
3677
|
"screenId": "himi_sign_in",
|
|
3672
3678
|
"mode": "signin",
|
|
3673
|
-
"returnScreenId": "himi_account_settings"
|
|
3674
|
-
|
|
3679
|
+
"returnScreenId": "himi_account_settings",
|
|
3680
|
+
"modal": "1"
|
|
3681
|
+
},
|
|
3682
|
+
"presentation": "sheet"
|
|
3675
3683
|
}
|
|
3676
3684
|
}
|
|
3677
3685
|
},
|
|
@@ -5581,8 +5589,7 @@ const SCREENS = runtimeSetting({
|
|
|
5581
5589
|
"style": "primary",
|
|
5582
5590
|
"fullWidth": true,
|
|
5583
5591
|
"action": {
|
|
5584
|
-
"actionId": "
|
|
5585
|
-
"presentation": "back"
|
|
5592
|
+
"actionId": "tier5.himiStoreAccount.leave"
|
|
5586
5593
|
}
|
|
5587
5594
|
}
|
|
5588
5595
|
}
|
|
@@ -5623,7 +5630,7 @@ const SCREENS = runtimeSetting({
|
|
|
5623
5630
|
"declaredCapabilities": [
|
|
5624
5631
|
"wix_api"
|
|
5625
5632
|
],
|
|
5626
|
-
"minBinaryVersion": "
|
|
5633
|
+
"minBinaryVersion": "1.7.0",
|
|
5627
5634
|
"bundleVersion": "0.0.1"
|
|
5628
5635
|
}
|
|
5629
5636
|
},
|
|
@@ -6490,8 +6497,10 @@ const SCREENS = runtimeSetting({
|
|
|
6490
6497
|
"screenId": "himi_sign_in",
|
|
6491
6498
|
"mode": "signin",
|
|
6492
6499
|
"intent": "profile",
|
|
6493
|
-
"returnScreenId": "himi_view_profile"
|
|
6494
|
-
|
|
6500
|
+
"returnScreenId": "himi_view_profile",
|
|
6501
|
+
"modal": "1"
|
|
6502
|
+
},
|
|
6503
|
+
"presentation": "sheet"
|
|
6495
6504
|
}
|
|
6496
6505
|
}
|
|
6497
6506
|
}
|
|
@@ -15317,8 +15326,10 @@ const SCREENS = runtimeSetting({
|
|
|
15317
15326
|
"screenId": "himi_sign_in",
|
|
15318
15327
|
"mode": "signin",
|
|
15319
15328
|
"intent": "orders",
|
|
15320
|
-
"returnScreenId": "himi_orders"
|
|
15321
|
-
|
|
15329
|
+
"returnScreenId": "himi_orders",
|
|
15330
|
+
"modal": "1"
|
|
15331
|
+
},
|
|
15332
|
+
"presentation": "sheet"
|
|
15322
15333
|
}
|
|
15323
15334
|
}
|
|
15324
15335
|
}
|
|
@@ -15951,8 +15962,10 @@ const SCREENS = runtimeSetting({
|
|
|
15951
15962
|
"screenId": "himi_sign_in",
|
|
15952
15963
|
"mode": "signin",
|
|
15953
15964
|
"intent": "addresses",
|
|
15954
|
-
"returnScreenId": "himi_addresses"
|
|
15955
|
-
|
|
15965
|
+
"returnScreenId": "himi_addresses",
|
|
15966
|
+
"modal": "1"
|
|
15967
|
+
},
|
|
15968
|
+
"presentation": "sheet"
|
|
15956
15969
|
}
|
|
15957
15970
|
}
|
|
15958
15971
|
}
|
|
@@ -17008,8 +17021,10 @@ const SCREENS = runtimeSetting({
|
|
|
17008
17021
|
"screenId": "himi_sign_in",
|
|
17009
17022
|
"mode": "signin",
|
|
17010
17023
|
"intent": "addresses",
|
|
17011
|
-
"returnScreenId": "himi_address"
|
|
17012
|
-
|
|
17024
|
+
"returnScreenId": "himi_address",
|
|
17025
|
+
"modal": "1"
|
|
17026
|
+
},
|
|
17027
|
+
"presentation": "sheet"
|
|
17013
17028
|
}
|
|
17014
17029
|
}
|
|
17015
17030
|
}
|
|
@@ -17679,8 +17694,10 @@ const SCREENS = runtimeSetting({
|
|
|
17679
17694
|
"screenId": "himi_sign_in",
|
|
17680
17695
|
"mode": "signin",
|
|
17681
17696
|
"intent": "wallet",
|
|
17682
|
-
"returnScreenId": "himi_wallet"
|
|
17683
|
-
|
|
17697
|
+
"returnScreenId": "himi_wallet",
|
|
17698
|
+
"modal": "1"
|
|
17699
|
+
},
|
|
17700
|
+
"presentation": "sheet"
|
|
17684
17701
|
}
|
|
17685
17702
|
}
|
|
17686
17703
|
}
|
|
@@ -18177,8 +18194,10 @@ const SCREENS = runtimeSetting({
|
|
|
18177
18194
|
"action": {
|
|
18178
18195
|
"actionId": "himi.open",
|
|
18179
18196
|
"params": {
|
|
18180
|
-
"screenId": "himi_sign_in"
|
|
18181
|
-
|
|
18197
|
+
"screenId": "himi_sign_in",
|
|
18198
|
+
"modal": "1"
|
|
18199
|
+
},
|
|
18200
|
+
"presentation": "sheet"
|
|
18182
18201
|
}
|
|
18183
18202
|
}
|
|
18184
18203
|
},
|
|
@@ -20780,8 +20799,10 @@ const SCREENS = runtimeSetting({
|
|
|
20780
20799
|
"screenId": "himi_sign_in",
|
|
20781
20800
|
"mode": "signin",
|
|
20782
20801
|
"intent": "post",
|
|
20783
|
-
"returnScreenId": "himi_create_post"
|
|
20784
|
-
|
|
20802
|
+
"returnScreenId": "himi_create_post",
|
|
20803
|
+
"modal": "1"
|
|
20804
|
+
},
|
|
20805
|
+
"presentation": "sheet"
|
|
20785
20806
|
}
|
|
20786
20807
|
}
|
|
20787
20808
|
}
|
|
@@ -28627,8 +28648,10 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
28627
28648
|
"params": {
|
|
28628
28649
|
"screenId": "himi_sign_in",
|
|
28629
28650
|
"returnScreenId": "himi_order",
|
|
28630
|
-
"returnId": "$service:himiBrandedOrder.id"
|
|
28631
|
-
|
|
28651
|
+
"returnId": "$service:himiBrandedOrder.id",
|
|
28652
|
+
"modal": "1"
|
|
28653
|
+
},
|
|
28654
|
+
"presentation": "sheet"
|
|
28632
28655
|
}
|
|
28633
28656
|
}
|
|
28634
28657
|
},
|
|
@@ -32145,8 +32168,10 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
32145
32168
|
"params": {
|
|
32146
32169
|
"screenId": "himi_sign_in",
|
|
32147
32170
|
"mode": "signin",
|
|
32148
|
-
"intent": "rsvp"
|
|
32149
|
-
|
|
32171
|
+
"intent": "rsvp",
|
|
32172
|
+
"modal": "1"
|
|
32173
|
+
},
|
|
32174
|
+
"presentation": "sheet"
|
|
32150
32175
|
}
|
|
32151
32176
|
}
|
|
32152
32177
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withOnlineWrites } from './online-actions.js';
|
|
1
2
|
import { defineService as register, type ServiceDefinition } from '@himalaya/state';
|
|
2
3
|
import { COLLECTIONS, COLLECTION_CHROME_MIN_ITEMS, initialCollection, normalizeCollection, projectCollection, collectionId, collectionActionsLabel, type CollectionRow } from './collection-contract.js';
|
|
3
4
|
import { localPreferences, type PreferenceRuntime } from './local-preferences.js';
|
|
@@ -331,6 +332,8 @@ export function decorateCollection(name: string, def: { state: State; actions?:
|
|
|
331
332
|
* reducer and reads the private controller snapshot, never an SDUI payload. */
|
|
332
333
|
export function defineBackendCollection<S extends object, Row extends object>(name: string, options: {
|
|
333
334
|
register?: typeof register;
|
|
335
|
+
/** Explicit mutation preflight; unrelated collection services keep their current reducers. */
|
|
336
|
+
onlineWrites?: boolean;
|
|
334
337
|
/** Revalidate already opened pages on navigation return; explicit refresh resets. */
|
|
335
338
|
restorePagesOnLoad?: boolean;
|
|
336
339
|
/** The load action's argument (onAppear params beyond `copy`), before the query runs — for a
|
|
@@ -350,7 +353,8 @@ export function defineBackendCollection<S extends object, Row extends object>(na
|
|
|
350
353
|
controller: controller as unknown as BackendCollection['controller'],
|
|
351
354
|
apply: (state, page) => options.apply(state as S, page as unknown as CollectionQueryState<Row>) as State,
|
|
352
355
|
});
|
|
353
|
-
const
|
|
356
|
+
const registerService = options.onlineWrites ? withOnlineWrites(options.register ?? register, ['confirmCancel', 'confirmOperation', 'retryOperation', 'commitWrite'], { messageFields: ['actionMessage'] }) : (options.register ?? register);
|
|
357
|
+
const handle = registerService(name, definition);
|
|
354
358
|
// Registration restored the state a cold boot saved: the page it showed is this run's, before the first load. Only
|
|
355
359
|
// when the service's own rows came back with it: a bundle that keeps them out of the restore (its `notHydrated`)
|
|
356
360
|
// starts its first load as before, never from rows it chose not to restore. Rows that came back empty are the
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { connectivity, defineService as register, type ServiceDefinition } from '@himalaya/state';
|
|
2
|
+
|
|
3
|
+
// Template generation enables this behavior only in the selected two starter families.
|
|
4
|
+
export const offlineWritesEnabled = false;
|
|
5
|
+
|
|
6
|
+
export const offlineWriteMessage = 'You’re offline. Connect to save this change, then submit it again.';
|
|
7
|
+
|
|
8
|
+
/** The guard runs before the reducer: entered values survive and no optimistic
|
|
9
|
+
* changes, success state, or queued writes can occur. It never submits on reconnect. */
|
|
10
|
+
export function offlineWriteState<S extends object>(state: S): S {
|
|
11
|
+
const next: Record<string, unknown> = { ...state, error: offlineWriteMessage, message: offlineWriteMessage };
|
|
12
|
+
for (const key of ['saving', 'isSaving', 'submitting', 'isSubmitting', 'adding', 'added', 'bookingInFlight']) {
|
|
13
|
+
if (key in next) next[key] = false;
|
|
14
|
+
}
|
|
15
|
+
for (const key of ['savedMessage', 'successMessage']) {
|
|
16
|
+
if (key in next) next[key] = '';
|
|
17
|
+
}
|
|
18
|
+
// A saved/favourited item is business state, not a submission-success flag.
|
|
19
|
+
return next as S;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface OfflineWriteOptions<S> {
|
|
23
|
+
derive?: (state: S) => S;
|
|
24
|
+
/** Exempt only non-mutating branches of a registered action. */
|
|
25
|
+
requiresConnectivity?: (action: string, state: S, argument?: unknown) => boolean;
|
|
26
|
+
messageFields?: readonly string[] | ((action: string) => readonly string[]);
|
|
27
|
+
draftFields?: readonly string[] | Readonly<Record<string, string>>;
|
|
28
|
+
secretFields?: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function withOnlineWrites<S extends object>(registerService: typeof register, names: readonly string[], options: OfflineWriteOptions<S> = {}): typeof register {
|
|
32
|
+
if (!offlineWritesEnabled) return registerService;
|
|
33
|
+
return ((name: string, definition: ServiceDefinition<Record<string, unknown>>) => {
|
|
34
|
+
let currentState: (() => Record<string, unknown>) | undefined;
|
|
35
|
+
const handle = registerService(name, {
|
|
36
|
+
...definition,
|
|
37
|
+
notHydrated: [...new Set([...(definition.notHydrated ?? []), ...(options.secretFields ?? [])])],
|
|
38
|
+
actions: Object.fromEntries(Object.entries(definition.actions ?? {}).map(([action, reduce]) => [action,
|
|
39
|
+
(state: Record<string, unknown>, ...args: never[]) => {
|
|
40
|
+
if (!names.includes(action)) return reduce(state, ...args);
|
|
41
|
+
if (options.requiresConnectivity?.(action, state as S, args[0]) === false) return reduce(state, ...args);
|
|
42
|
+
const fields = typeof options.messageFields === 'function' ? options.messageFields(action) : options.messageFields ?? ['message'];
|
|
43
|
+
if (connectivity().online) {
|
|
44
|
+
let cleared = false;
|
|
45
|
+
for (const field of ['error', 'message', ...fields]) {
|
|
46
|
+
if (state[field] === offlineWriteMessage) { state[field] = ''; cleared = true; }
|
|
47
|
+
}
|
|
48
|
+
// Keep the runtime's state object: reducers may mutate it and return undefined.
|
|
49
|
+
const result = reduce(state, ...args);
|
|
50
|
+
if (!cleared) return result;
|
|
51
|
+
// Publish cleared feedback even for a void reducer. An async completion must
|
|
52
|
+
// keep any newer state published by another action while it was awaiting.
|
|
53
|
+
const finish = (next: Record<string, unknown> | undefined) => next === undefined ? currentState?.() ?? state : next;
|
|
54
|
+
return result && typeof (result as Promise<unknown>).then === 'function'
|
|
55
|
+
? Promise.resolve(result).then(finish)
|
|
56
|
+
: finish(result as Record<string, unknown> | undefined);
|
|
57
|
+
}
|
|
58
|
+
const draft = { ...state };
|
|
59
|
+
const argument = args[0] as Record<string, unknown> | undefined;
|
|
60
|
+
const draftFields = options.draftFields ?? [];
|
|
61
|
+
const entries = Array.isArray(draftFields) ? draftFields.map(key => [key, key]) : Object.entries(draftFields);
|
|
62
|
+
for (const [key, source] of entries) {
|
|
63
|
+
if (argument && key in state && typeof argument[source] === typeof state[key]) draft[key] = argument[source];
|
|
64
|
+
}
|
|
65
|
+
for (const key of options.secretFields ?? []) draft[key] = '';
|
|
66
|
+
const feedback = Object.fromEntries(fields.map(key => [key, offlineWriteMessage]));
|
|
67
|
+
let next = { ...offlineWriteState(draft), ...feedback };
|
|
68
|
+
if (options.derive) next = options.derive(next as S) as Record<string, unknown>;
|
|
69
|
+
return { ...next, ...feedback };
|
|
70
|
+
},
|
|
71
|
+
])),
|
|
72
|
+
});
|
|
73
|
+
if (typeof handle.getState === 'function') currentState = () => handle.getState();
|
|
74
|
+
return handle;
|
|
75
|
+
}) as typeof register;
|
|
76
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { withOnlineWrites } from "../_shared/online-actions.js";
|
|
1
2
|
// Tier-5 bundle: Branded Lite — add / edit one saved address (himi_address).
|
|
2
3
|
//
|
|
3
4
|
// No `id` nav param → a new address; `?id=` → that address, prefilled. Save writes it
|
|
4
5
|
// to the member's address book (@himalaya/wix-ecom) and goes back to My Addresses,
|
|
5
6
|
// which reloads on appear. Edit also offers Remove, behind an in-screen confirm.
|
|
6
|
-
import { defineService, navigate } from "@himalaya/state";
|
|
7
|
+
import { defineService as registerService, navigate } from "@himalaya/state";
|
|
7
8
|
import {
|
|
8
9
|
createMemberAddress,
|
|
9
10
|
deleteMemberAddress,
|
|
@@ -176,6 +177,7 @@ async function remove(state: State): Promise<State> {
|
|
|
176
177
|
}
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
const defineService = withOnlineWrites(registerService, ["save", "remove"], { derive });
|
|
179
181
|
const svc = defineService<State, {
|
|
180
182
|
load: (state: State, arg?: unknown) => Promise<State>;
|
|
181
183
|
retry: (state: State) => Promise<State>;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { withOnlineWrites } from "../_shared/online-actions.js";
|
|
1
2
|
// Tier-5 bundle: Branded Lite — My Addresses (the member's saved address book).
|
|
2
3
|
//
|
|
3
4
|
// The same book the site's Members Area "My Addresses" page and checkout use
|
|
4
5
|
// (addresses-web, see @himalaya/wix-ecom listMemberAddresses). A member sees every
|
|
5
6
|
// saved address, picks the default, and removes one after an in-screen confirm;
|
|
6
7
|
// add/edit open himi_address. A guest gets the shared member gate.
|
|
7
|
-
import { defineService, navigate } from "@himalaya/state";
|
|
8
|
+
import { defineService as registerService, navigate } from "@himalaya/state";
|
|
8
9
|
import {
|
|
9
10
|
deleteMemberAddress,
|
|
10
11
|
listMemberAddresses,
|
|
@@ -152,6 +153,7 @@ async function write(state: State, run: () => Promise<unknown>, done: string, fa
|
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
const defineService = withOnlineWrites(registerService, ["setDefault", "remove"], { derive });
|
|
155
157
|
const svc = defineService<State, {
|
|
156
158
|
load: (state: State, arg?: unknown) => Promise<State>;
|
|
157
159
|
retry: (state: State) => Promise<State>;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { withOnlineWrites } from "../_shared/online-actions.js";
|
|
2
|
+
import { defineService as registerState } from "@himalaya/state";
|
|
1
3
|
import { getStoreProduct } from "../_shared/himi-store.js";
|
|
2
4
|
import { iapPriceCopy } from "../_shared/iap-worker.js";
|
|
3
5
|
import { withIapPresentation } from "../_shared/iap-presentation.js";
|
|
@@ -378,6 +380,7 @@ function couponDetails(result: unknown): { id: string; code: string } {
|
|
|
378
380
|
|
|
379
381
|
let cartPricing: IapCommerce;
|
|
380
382
|
const defineService = withIapPresentation<State>({
|
|
383
|
+
register: withOnlineWrites(registerState, ["remove", "increment", "decrement", "checkout", "beginPromo", "applyPromo", "removePromo"], { derive, messageFields: action => action.includes("Promo") ? ["promoError"] : ["checkoutHint"] }),
|
|
381
384
|
onCreate: commerce => { cartPricing = commerce; },
|
|
382
385
|
prefix: "cart", transientKeys: ["items", "subtotal", "promoBusy"], contentActions: ["load", "remove", "increment", "decrement"],
|
|
383
386
|
items: state => state.items.flatMap(row => row.catalogReference?.catalogItemId ? [{ reference: { kind: "storesProduct", id: row.catalogReference.catalogItemId }, commerceDisplayPrice: row.commerceTotal ?? row.total }] : []),
|