@wix/himalaya-cli 0.1570.0 → 0.1572.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/ts/src/define-app.d.ts +8 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/auth-providers/wix/src/index.d.ts +7 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/blog-v3/src/extensions.d.ts +6 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/members-feed-viewer/src/index.d.ts +6 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/auth.mjs +17 -7
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core/validation.mjs +17 -7
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +17 -7
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +17 -7
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/state.mjs +17 -7
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-auth.mjs +7 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-blog.mjs +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-members-feed-viewer.mjs +18 -0
- package/dist/cli.mjs +2014 -1252
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/base44-app-to-himi-app.md +1 -1
- package/guides/capabilities.md +7 -0
- package/guides/himi-json-output.md +11 -0
- package/guides/index.json +7 -0
- package/guides/map-one-app-coverage.js +117 -0
- package/guides/one-app-to-himi-app.md +209 -11
- package/guides/recipe-one-app-coverage-mapping.md +74 -0
- package/guides/sdk-reference.json +8 -2
- package/guides/splash-screens.md +10 -1
- package/guides/trim-a-template.md +5 -0
- package/guides/wix-business-context.md +11 -2
- package/package.json +2 -2
- package/templates/beauty-salon/config.ts +1 -1
- package/templates/beauty-salon/dev/index.ts +31 -1
- package/templates/beauty-salon/tier5-src/_shared/member-read.ts +52 -0
- package/templates/beauty-salon/tier5-src/_shared/program-description.ts +11 -0
- package/templates/beauty-salon/tier5-src/himi-blog-post/index.ts +35 -12
- package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +3 -3
- package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +6 -9
- package/templates/beauty-salon-owner/config.ts +1 -1
- package/templates/beauty-salon-owner/dev/index.ts +2 -3
- package/templates/branded-lite/config.ts +1 -1
- package/templates/branded-lite/dev/index.ts +163 -2
- package/templates/branded-lite/tier5-src/_shared/member-read.ts +52 -0
- package/templates/branded-lite/tier5-src/_shared/program-description.ts +11 -0
- package/templates/branded-lite/tier5-src/himi-blog-post/index.ts +35 -12
- package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +3 -3
- package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +16 -14
- package/templates/branded-lite/tier5-src/himi-branded-lite-programs/index.ts +31 -18
- package/templates/branded-lite/tier5-src/himi-events-my-events/index.ts +76 -42
- package/templates/branded-lite/tier5-src/himi-store-product/index.ts +6 -9
- package/templates/branded-lite/tier5-src/wix-spaces-chat-thread/index.ts +101 -34
- package/templates/branded-lite/tier5-src/wix-spaces-forum-post/index.ts +23 -6
- package/templates/branded-lite/tier5-src/wix-spaces-member-detail/index.ts +26 -11
- package/templates/community-club/config.ts +1 -1
- package/templates/community-club/dev/index.ts +65 -2
- package/templates/community-club/tier5-src/_shared/branded-load-state.ts +106 -0
- package/templates/community-club/tier5-src/_shared/member-read.ts +52 -0
- package/templates/community-club/tier5-src/_shared/program-description.ts +11 -0
- package/templates/community-club/tier5-src/himi-blog-post/index.ts +35 -12
- package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +3 -3
- package/templates/community-club/tier5-src/himi-events-my-events/index.ts +76 -42
- package/templates/community-club-owner/config.ts +1 -1
- package/templates/community-club-owner/dev/index.ts +2 -3
- package/templates/events-meetups/config.ts +1 -1
- package/templates/events-meetups/dev/index.ts +65 -2
- package/templates/events-meetups/tier5-src/_shared/branded-load-state.ts +106 -0
- package/templates/events-meetups/tier5-src/_shared/member-read.ts +52 -0
- package/templates/events-meetups/tier5-src/_shared/program-description.ts +11 -0
- package/templates/events-meetups/tier5-src/himi-blog-post/index.ts +35 -12
- package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +3 -3
- package/templates/events-meetups/tier5-src/himi-events-my-events/index.ts +76 -42
- package/templates/events-meetups-owner/config.ts +1 -1
- package/templates/events-meetups-owner/dev/index.ts +2 -3
- package/templates/gym-fitness/config.ts +1 -1
- package/templates/gym-fitness/dev/index.ts +65 -2
- package/templates/gym-fitness/tier5-src/_shared/member-read.ts +52 -0
- package/templates/gym-fitness/tier5-src/_shared/program-description.ts +11 -0
- package/templates/gym-fitness/tier5-src/himi-blog-post/index.ts +35 -12
- package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +3 -3
- package/templates/gym-fitness/tier5-src/himi-events-my-events/index.ts +76 -42
- package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +6 -9
- package/templates/gym-fitness-owner/config.ts +1 -1
- package/templates/gym-fitness-owner/dev/index.ts +2 -3
- package/templates/nonprofit-cause/config.ts +1 -1
- package/templates/nonprofit-cause/dev/index.ts +65 -2
- package/templates/nonprofit-cause/tier5-src/_shared/branded-load-state.ts +106 -0
- package/templates/nonprofit-cause/tier5-src/_shared/member-read.ts +52 -0
- package/templates/nonprofit-cause/tier5-src/_shared/program-description.ts +11 -0
- package/templates/nonprofit-cause/tier5-src/himi-blog-post/index.ts +35 -12
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +3 -3
- package/templates/nonprofit-cause/tier5-src/himi-events-my-events/index.ts +76 -42
- package/templates/nonprofit-cause-owner/config.ts +1 -1
- package/templates/nonprofit-cause-owner/dev/index.ts +2 -3
- package/templates/owner-lite/config.ts +1 -1
- package/templates/owner-lite/dev/index.ts +2 -3
- package/templates/podcast-creator/config.ts +1 -1
- package/templates/podcast-creator/dev/index.ts +65 -2
- package/templates/podcast-creator/tier5-src/_shared/branded-load-state.ts +106 -0
- package/templates/podcast-creator/tier5-src/_shared/member-read.ts +52 -0
- package/templates/podcast-creator/tier5-src/_shared/program-description.ts +11 -0
- package/templates/podcast-creator/tier5-src/himi-blog-post/index.ts +35 -12
- package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +3 -3
- package/templates/podcast-creator/tier5-src/himi-events-my-events/index.ts +76 -42
- package/templates/podcast-creator-owner/config.ts +1 -1
- package/templates/podcast-creator-owner/dev/index.ts +2 -3
- package/templates/portfolio-creative/config.ts +1 -1
- package/templates/portfolio-creative/dev/index.ts +65 -2
- package/templates/portfolio-creative/tier5-src/_shared/branded-load-state.ts +106 -0
- package/templates/portfolio-creative/tier5-src/_shared/member-read.ts +52 -0
- package/templates/portfolio-creative/tier5-src/_shared/program-description.ts +11 -0
- package/templates/portfolio-creative/tier5-src/himi-blog-post/index.ts +35 -12
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +3 -3
- package/templates/portfolio-creative/tier5-src/himi-events-my-events/index.ts +76 -42
- package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +6 -9
- package/templates/portfolio-creative-owner/config.ts +1 -1
- package/templates/portfolio-creative-owner/dev/index.ts +2 -3
- package/templates/restaurant/config.ts +1 -1
- package/templates/restaurant/dev/index.ts +65 -2
- package/templates/restaurant/tier5-src/_shared/branded-load-state.ts +106 -0
- package/templates/restaurant/tier5-src/_shared/member-read.ts +52 -0
- package/templates/restaurant/tier5-src/_shared/program-description.ts +11 -0
- package/templates/restaurant/tier5-src/himi-blog-post/index.ts +35 -12
- package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +3 -3
- package/templates/restaurant/tier5-src/himi-events-my-events/index.ts +76 -42
- package/templates/restaurant-owner/config.ts +1 -1
- package/templates/restaurant-owner/dev/index.ts +2 -3
- package/templates/retail-store/config.ts +1 -1
- package/templates/retail-store/dev/index.ts +31 -1
- package/templates/retail-store/tier5-src/_shared/branded-load-state.ts +106 -0
- package/templates/retail-store/tier5-src/_shared/member-read.ts +52 -0
- package/templates/retail-store/tier5-src/_shared/program-description.ts +11 -0
- package/templates/retail-store/tier5-src/himi-blog-post/index.ts +35 -12
- package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +3 -3
- package/templates/retail-store/tier5-src/himi-store-product/index.ts +6 -9
- package/templates/retail-store-owner/config.ts +1 -1
- package/templates/retail-store-owner/dev/index.ts +2 -3
- package/templates/yoga-studio/config.ts +1 -1
- package/templates/yoga-studio/dev/index.ts +93 -2
- package/templates/yoga-studio/tier5-src/_shared/member-read.ts +52 -0
- package/templates/yoga-studio/tier5-src/_shared/program-description.ts +11 -0
- package/templates/yoga-studio/tier5-src/himi-blog-post/index.ts +35 -12
- package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +3 -3
- package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +16 -14
- package/templates/yoga-studio/tier5-src/himi-branded-lite-programs/index.ts +31 -18
- package/templates/yoga-studio/tier5-src/himi-events-my-events/index.ts +76 -42
- package/templates/yoga-studio-owner/config.ts +1 -1
- package/templates/yoga-studio-owner/dev/index.ts +2 -3
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// `chatRealtime` channel, else a 4 s foreground `_poll` of `refresh`. A message from the
|
|
10
10
|
// business then appears without the member leaving the screen. `load`, `setDraft` and
|
|
11
11
|
// `send` keep their contract; the directives are stripped by the runtime, never state.
|
|
12
|
-
import { defineService, type PollDirective, type StreamDirective } from "@himalaya/state";
|
|
12
|
+
import { defineService, type PollDirective, type PollStopDirective, type StreamDirective } from "@himalaya/state";
|
|
13
13
|
import { getMessages, sendChatMessage, getOrCreateDirectChatroom, getChatToken, type ChatMessage } from "@himalaya/wix-chat-web";
|
|
14
14
|
import { getMyData } from "@himalaya/wix-users";
|
|
15
15
|
import { formatMonthDay, formatMonthDayTime } from "../_shared/engine-intl.js";
|
|
@@ -17,7 +17,10 @@ import { initials } from "../_shared/himi-text.js";
|
|
|
17
17
|
import { copyParam, stashCopy } from "../_shared/himi-copy.js";
|
|
18
18
|
import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
19
19
|
import { writeMayHaveLanded } from "../_shared/owner-load-error.js";
|
|
20
|
-
import { chatRealtimeConfig, createReloadGate, createSendIntents, createVisitToken, liveDirectives, postWithVisitToken, liveFallback, type ChatLiveMode, type ChatRealtimeConfig } from "../_shared/chat-live.js";
|
|
20
|
+
import { CHAT_LIVE_POLL_ID, chatRealtimeConfig, createReloadGate, createSendIntents, createVisitToken, liveDirectives, postWithVisitToken, isTokenRejected, liveFallback, type ChatLiveMode, type ChatRealtimeConfig } from "../_shared/chat-live.js";
|
|
21
|
+
|
|
22
|
+
import { memberAccess, memberAccessChanged } from "../_shared/member-access.js";
|
|
23
|
+
import { readAccess, assertReadAccess, readAccessDenied, NO_READ_SIGN_IN, type ReadAccess } from "../_shared/member-read.js";
|
|
21
24
|
|
|
22
25
|
const SERVICE = "spacesChatThread";
|
|
23
26
|
const EMPTY_TEXT = "No messages yet. Say hello!";
|
|
@@ -31,13 +34,14 @@ interface Row {
|
|
|
31
34
|
imageUrl: string; imageDescription: string; imageHidden: boolean; dayLabel: string; dayHidden: boolean;
|
|
32
35
|
}
|
|
33
36
|
interface S extends PresenceFlags {
|
|
37
|
+
threadMember: ReadAccess | null; signInRequired: boolean; noSignIn: boolean;
|
|
34
38
|
status: "loading" | "ready"; messages: Row[]; emptyText: string; emptyHidden: boolean; draft: string; sendHint: string; resolvedChatroomId: string; myUserId: string;
|
|
35
39
|
/** Send token for the host's transcript scroll: a new value follows the tail. */
|
|
36
40
|
scrollSend: string;
|
|
37
41
|
/** Which path keeps the open thread live (diagnostic). */
|
|
38
42
|
live?: ChatLiveMode;
|
|
39
43
|
_stream?: StreamDirective;
|
|
40
|
-
_poll?: PollDirective;
|
|
44
|
+
_poll?: PollDirective | PollStopDirective;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
function inputId(): string {
|
|
@@ -125,15 +129,21 @@ function nextScrollToken(prev: string): string {
|
|
|
125
129
|
// mint a token every 4 s on top of its GET. Reset on every `load`.
|
|
126
130
|
const memberToken = createVisitToken(() => getChatToken({ role: "member" }).then((t) => t.chatToken));
|
|
127
131
|
|
|
128
|
-
async function reload(id: string, myUserId: string): Promise<Row[]> {
|
|
132
|
+
async function reload(id: string, myUserId: string, member: ReadAccess): Promise<Row[]> {
|
|
129
133
|
if (!id) return [];
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
for (let attempt = 0; ; attempt++) {
|
|
135
|
+
const token = memberToken.get();
|
|
136
|
+
try {
|
|
137
|
+
const chatToken = await token;
|
|
138
|
+
await assertReadAccess(member);
|
|
139
|
+
const msgs = await getMessages(id, 30, { role: "member", chatToken });
|
|
140
|
+
return rowsOf(msgs ?? [], myUserId);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
memberToken.drop(token);
|
|
143
|
+
// An expired visit token gets one fresh attempt. A repeated refusal still
|
|
144
|
+
// clears private history; checking identity before each read prevents stale retries.
|
|
145
|
+
if (attempt > 0 || !isTokenRejected(error)) throw error;
|
|
146
|
+
}
|
|
137
147
|
}
|
|
138
148
|
}
|
|
139
149
|
|
|
@@ -142,9 +152,13 @@ const intents = createSendIntents();
|
|
|
142
152
|
|
|
143
153
|
/** Post once per intent: a retried tap of the same text reuses its message id, so a send
|
|
144
154
|
* that failed ambiguously and is tapped again stays one message. */
|
|
145
|
-
async function postOnce(chatroomId: string, text: string): Promise<void> {
|
|
155
|
+
async function postOnce(chatroomId: string, text: string, member: ReadAccess): Promise<void> {
|
|
146
156
|
const messageId = intents.idFor(chatroomId, text);
|
|
147
|
-
await postWithVisitToken(memberToken, (chatToken) =>
|
|
157
|
+
await postWithVisitToken(memberToken, async (chatToken) => {
|
|
158
|
+
await assertReadAccess(member);
|
|
159
|
+
if (liveState().threadMember !== member) throw memberAccessChanged();
|
|
160
|
+
return sendChatMessage(chatroomId, text, { role: "member", chatToken, messageId });
|
|
161
|
+
});
|
|
148
162
|
intents.settle(chatroomId, text);
|
|
149
163
|
}
|
|
150
164
|
|
|
@@ -155,7 +169,7 @@ let sendingNow = false;
|
|
|
155
169
|
// The conversation a live re-read reads, and the channel the screen passed (if any).
|
|
156
170
|
let current = { chatroomId: "", myUserId: "" };
|
|
157
171
|
let realtime: ChatRealtimeConfig | null = null;
|
|
158
|
-
const gate = createReloadGate(() => reload(current.chatroomId, current.myUserId));
|
|
172
|
+
const gate = createReloadGate(() => reload(current.chatroomId, current.myUserId, liveState().threadMember!));
|
|
159
173
|
|
|
160
174
|
/** A tick or a stream event: re-read the open conversation. A failed tick keeps the
|
|
161
175
|
* thread as it is — the next one tries again.
|
|
@@ -167,12 +181,16 @@ const gate = createReloadGate(() => reload(current.chatroomId, current.myUserId)
|
|
|
167
181
|
async function reread(s: S): Promise<S> {
|
|
168
182
|
if (!s.found || !s.resolvedChatroomId || s.resolvedChatroomId !== current.chatroomId) return s;
|
|
169
183
|
try {
|
|
184
|
+
await assertThreadMember(s);
|
|
170
185
|
const messages = await gate.request();
|
|
186
|
+
await assertThreadMember(s);
|
|
171
187
|
const now = svc.getState();
|
|
172
|
-
if (now.resolvedChatroomId !== s.resolvedChatroomId) return now;
|
|
188
|
+
if (now.threadMember !== s.threadMember || now.resolvedChatroomId !== s.resolvedChatroomId) return now;
|
|
173
189
|
return { ...now, messages, emptyText: messages.length ? "" : now.emptyText || EMPTY_TEXT, emptyHidden: messages.length > 0 };
|
|
174
|
-
} catch {
|
|
175
|
-
|
|
190
|
+
} catch (error) {
|
|
191
|
+
const now = svc.getState();
|
|
192
|
+
if (now.threadMember !== s.threadMember) return now;
|
|
193
|
+
return readAccessDenied(error) ? threadFailure(error) : now;
|
|
176
194
|
}
|
|
177
195
|
}
|
|
178
196
|
|
|
@@ -187,40 +205,71 @@ async function resolveMyUserId(): Promise<string> {
|
|
|
187
205
|
try { return (await getMyData()).userId || ""; } catch { return ""; }
|
|
188
206
|
}
|
|
189
207
|
|
|
208
|
+
const INITIAL: S = { status: "loading", messages: [], emptyText: "", emptyHidden: true, scrollSend: "", draft: "", sendHint: "", resolvedChatroomId: "", myUserId: "", ...presence("loading"), ...NO_READ_SIGN_IN, threadMember: null };
|
|
209
|
+
let loadVersion = 0;
|
|
210
|
+
|
|
211
|
+
async function threadFailure(error: unknown): Promise<S> {
|
|
212
|
+
const version = loadVersion;
|
|
213
|
+
const member = await memberAccess();
|
|
214
|
+
const flags = member.email ? NO_READ_SIGN_IN : { signInRequired: true, noSignIn: false };
|
|
215
|
+
if (version !== loadVersion) return liveState();
|
|
216
|
+
current = { chatroomId: "", myUserId: "" };
|
|
217
|
+
memberToken.reset();
|
|
218
|
+
return { ...INITIAL, status: "ready", ...presenceAfterError(error), ...flags, live: "off", _poll: { id: CHAT_LIVE_POLL_ID, stop: true } };
|
|
219
|
+
}
|
|
220
|
+
async function assertThreadMember(s: S): Promise<void> {
|
|
221
|
+
if (!s.threadMember?.member?.email) throw new Error("Unauthenticated: sign in to read messages");
|
|
222
|
+
await assertReadAccess(s.threadMember);
|
|
223
|
+
}
|
|
224
|
+
|
|
190
225
|
const svc = defineService<S, {
|
|
191
|
-
|
|
226
|
+
prepare: (s: S) => S;
|
|
227
|
+
load: (s: S, a?: unknown) => Promise<S | undefined>;
|
|
192
228
|
setDraft: (s: S, a?: unknown) => S;
|
|
193
229
|
send: (s: S) => Promise<S>;
|
|
194
230
|
refresh: (s: S) => Promise<S>;
|
|
195
231
|
onStreamEvent: (s: S) => Promise<S>;
|
|
196
232
|
onStreamError: (s: S) => S;
|
|
197
233
|
}>(SERVICE, {
|
|
198
|
-
state:
|
|
234
|
+
state: INITIAL,
|
|
199
235
|
actions: {
|
|
200
|
-
|
|
236
|
+
prepare: (s) => ({ ...s, ...INITIAL, _poll: { id: CHAT_LIVE_POLL_ID, stop: true } }),
|
|
237
|
+
load: async (s, a): Promise<S | undefined> => {
|
|
238
|
+
const version = ++loadVersion;
|
|
239
|
+
await svc.prepare();
|
|
240
|
+
current = { chatroomId: "", myUserId: "" };
|
|
201
241
|
stashCopy(a);
|
|
202
242
|
const param = (a as { chatRealtime?: unknown } | null | undefined)?.chatRealtime;
|
|
203
243
|
if (param !== undefined) realtime = chatRealtimeConfig(param);
|
|
204
244
|
const raw = inputId();
|
|
205
245
|
// No thread / recipient id (a stale link, a route opened directly) -> the shared
|
|
206
246
|
// not-found state, and no composer: there is nobody to send to.
|
|
207
|
-
if (!detailInputId()) return { ...
|
|
247
|
+
if (!detailInputId()) return { ...INITIAL, status: "ready", messages: [], emptyText: "", emptyHidden: true, ...presence("missing") };
|
|
208
248
|
memberToken.reset();
|
|
209
|
-
// Resolve chatroom + my user id concurrently (myUserId tags own messages).
|
|
210
|
-
const [chatroomId, myUserId] = await Promise.all([resolveChatroomId(raw, inputKind()), resolveMyUserId()]);
|
|
211
|
-
// A recipient whose 1:1 chatroom could not be resolved is not a conversation yet:
|
|
212
|
-
// reload("") would return [] without throwing and publish a live, empty thread
|
|
213
|
-
// whose Send can only fail. Show the load-error line instead.
|
|
214
|
-
if (!chatroomId) return { ...s, status: "ready", resolvedChatroomId: "", myUserId, messages: [], emptyText: "", emptyHidden: true, ...presence("failed") };
|
|
215
249
|
try {
|
|
216
|
-
const
|
|
250
|
+
const member = await readAccess();
|
|
251
|
+
if (!member.member.email) throw new Error("Unauthenticated: sign in to read messages");
|
|
252
|
+
// Resolve chatroom + my user id concurrently (myUserId tags own messages).
|
|
253
|
+
const [chatroomId, myUserId] = await Promise.all([resolveChatroomId(raw, inputKind()), resolveMyUserId()]);
|
|
254
|
+
// A recipient whose 1:1 chatroom could not be resolved is not a conversation yet:
|
|
255
|
+
// reload("") would return [] without throwing and publish a live, empty thread
|
|
256
|
+
// whose Send can only fail. Show the load-error line instead.
|
|
257
|
+
if (!chatroomId) return version === loadVersion ? { ...INITIAL, status: "ready", ...presence("failed") } : undefined;
|
|
258
|
+
const messages = await reload(chatroomId, myUserId, member);
|
|
259
|
+
await assertReadAccess(member);
|
|
260
|
+
if (version !== loadVersion) return undefined;
|
|
217
261
|
current = { chatroomId, myUserId };
|
|
218
|
-
|
|
262
|
+
const previous = s.threadMember;
|
|
263
|
+
const sameReader = previous?.member.email === member.member.email && previous?.member.scope === member.member.scope && previous?.tenant === member.tenant;
|
|
264
|
+
const draft = sameReader && s.resolvedChatroomId === chatroomId ? s.draft : "";
|
|
265
|
+
return { ...INITIAL, draft, threadMember: member, status: "ready", resolvedChatroomId: chatroomId, myUserId, messages, emptyText: messages.length ? "" : EMPTY_TEXT, emptyHidden: messages.length > 0, scrollSend: messages.length ? nextScrollToken(s.scrollSend) : "", ...presence("found"), ...liveDirectives(realtime, chatroomId, SERVICE) };
|
|
219
266
|
} catch (e) {
|
|
220
267
|
// A thread whose messages cannot be read is not a live, empty conversation: a 404
|
|
221
268
|
// is the shared not-found state, anything else the load-error line — never a
|
|
222
269
|
// "Say hello!" composer that can only fail.
|
|
223
|
-
|
|
270
|
+
if (version !== loadVersion) return undefined;
|
|
271
|
+
const failed = await threadFailure(e);
|
|
272
|
+
return version === loadVersion ? failed : undefined;
|
|
224
273
|
}
|
|
225
274
|
},
|
|
226
275
|
setDraft: (s, a) => ({ ...s, draft: val(a) }),
|
|
@@ -234,9 +283,14 @@ const svc = defineService<S, {
|
|
|
234
283
|
// Every return builds on the LIVE state: the runtime replaces state wholesale, and ticks
|
|
235
284
|
// or typing may have changed it while this awaited.
|
|
236
285
|
send: async (s) => {
|
|
286
|
+
if (!s.found) return s;
|
|
237
287
|
const text = s.draft.trim();
|
|
238
|
-
|
|
239
|
-
|
|
288
|
+
try { await assertThreadMember(s); }
|
|
289
|
+
catch (error) { return liveState().threadMember !== s.threadMember ? liveState() : threadFailure(error); }
|
|
290
|
+
if (liveState().threadMember !== s.threadMember) return liveState();
|
|
291
|
+
const checked = liveState();
|
|
292
|
+
if (!text) return checked.draft.trim() ? checked : { ...checked, sendHint: "Type a message first." };
|
|
293
|
+
if (sendingNow) return checked;
|
|
240
294
|
sendingNow = true;
|
|
241
295
|
try {
|
|
242
296
|
// Resolve lazily in case send() runs before load() settled.
|
|
@@ -244,8 +298,11 @@ const svc = defineService<S, {
|
|
|
244
298
|
if (!chatroomId) chatroomId = await resolveChatroomId(inputId(), inputKind());
|
|
245
299
|
if (!chatroomId) return { ...liveState(), sendHint: "Couldn't start conversation." };
|
|
246
300
|
try {
|
|
247
|
-
await postOnce(chatroomId, text);
|
|
301
|
+
await postOnce(chatroomId, text, s.threadMember!);
|
|
248
302
|
} catch (e) {
|
|
303
|
+
if (liveState().threadMember !== s.threadMember) return liveState();
|
|
304
|
+
try { await assertThreadMember(s); }
|
|
305
|
+
catch (error) { return threadFailure(error); }
|
|
249
306
|
// Surface a short reason (the SDK rejects with "HTTP <code>: <body>") so a
|
|
250
307
|
// real failure is diagnosable on-device instead of an opaque "Couldn't send".
|
|
251
308
|
// A failure that may have posted anyway (a gateway timeout after the backend stored
|
|
@@ -255,6 +312,8 @@ const svc = defineService<S, {
|
|
|
255
312
|
if (writeMayHaveLanded(e)) return { ...liveState(), resolvedChatroomId: chatroomId, sendHint: "Couldn't confirm it was sent — check before sending again." };
|
|
256
313
|
return { ...liveState(), resolvedChatroomId: chatroomId, sendHint: msg ? `Couldn't send — ${msg.slice(0, 80)}` : "Couldn't send — try again." };
|
|
257
314
|
}
|
|
315
|
+
await assertThreadMember(s);
|
|
316
|
+
if (liveState().threadMember !== s.threadMember) return liveState();
|
|
258
317
|
// SENT. From here on nothing may say otherwise: a failed re-read must not offer a
|
|
259
318
|
// second Send of a message that already posted.
|
|
260
319
|
const myUserId = s.myUserId || (await resolveMyUserId());
|
|
@@ -264,18 +323,26 @@ const svc = defineService<S, {
|
|
|
264
323
|
const cleared = (now: S): string => (now.draft.trim() === text ? "" : now.draft);
|
|
265
324
|
try {
|
|
266
325
|
const messages = await gate.request();
|
|
326
|
+
await assertThreadMember(s);
|
|
327
|
+
if (liveState().threadMember !== s.threadMember) return liveState();
|
|
267
328
|
const now = liveState();
|
|
268
329
|
return { ...now, resolvedChatroomId: chatroomId, myUserId, messages, draft: cleared(now), emptyText: "", emptyHidden: true, scrollSend: nextScrollToken(now.scrollSend), sendHint: "Sent ✓" };
|
|
269
|
-
} catch {
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (liveState().threadMember !== s.threadMember) return liveState();
|
|
332
|
+
if (readAccessDenied(error)) return threadFailure(error);
|
|
270
333
|
const now = liveState();
|
|
271
334
|
const local: Row = { id: `local-${now.messages.length}`, text, when: "", leadFill: " ", mine: true, notMine: false, senderBadge: "", avatarUrl: "", avatarInitials: "", imageUrl: "", imageDescription: "", imageHidden: true, dayLabel: "", dayHidden: true };
|
|
272
335
|
return { ...now, resolvedChatroomId: chatroomId, myUserId, messages: [...now.messages, local], draft: cleared(now), emptyText: "", emptyHidden: true, scrollSend: nextScrollToken(now.scrollSend), sendHint: "Sent ✓" };
|
|
273
336
|
}
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if (liveState().threadMember !== s.threadMember) return liveState();
|
|
339
|
+
return threadFailure(error);
|
|
274
340
|
} finally {
|
|
275
341
|
sendingNow = false;
|
|
276
342
|
}
|
|
277
343
|
},
|
|
278
344
|
},
|
|
279
345
|
});
|
|
346
|
+
svc.prepare();
|
|
280
347
|
svc.load();
|
|
281
348
|
export { svc };
|
|
@@ -7,7 +7,10 @@ import { presence, presenceAfterError, detailInputId, type PresenceFlags } from
|
|
|
7
7
|
import { openRichLink } from "../_shared/rich-content-link.js";
|
|
8
8
|
import { richBody } from "../_shared/rich-body.js";
|
|
9
9
|
|
|
10
|
+
import { readAccess, assertReadAccess, readSignInFlags, NO_READ_SIGN_IN } from "../_shared/member-read.js";
|
|
11
|
+
|
|
10
12
|
interface S extends PresenceFlags {
|
|
13
|
+
signInRequired: boolean; noSignIn: boolean;
|
|
11
14
|
status: "loading" | "ready"; id: string; title: string; author: string; subtitle: string;
|
|
12
15
|
/** The plain-text body (`contentText`): RichContent's fallback, and what shows when there is no document. */
|
|
13
16
|
body: string;
|
|
@@ -26,8 +29,12 @@ function val(arg?: unknown): string {
|
|
|
26
29
|
return typeof arg === "string" ? arg : ((arg as { value?: string; text?: string })?.value ?? (arg as { text?: string })?.text ?? "");
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
const INITIAL: S = { status: "loading", id: "", title: "", author: "", subtitle: "", body: "", bodyJson: "", hasBodyJson: false, initial: "", likeHint: "", commentText: "", commentHint: "", ...presence("loading"), ...NO_READ_SIGN_IN };
|
|
33
|
+
let loadVersion = 0;
|
|
34
|
+
|
|
29
35
|
const svc = defineService<S, {
|
|
30
|
-
|
|
36
|
+
prepare: (s: S) => S;
|
|
37
|
+
load: (s: S) => Promise<S | undefined>;
|
|
31
38
|
like: (s: S) => Promise<S>;
|
|
32
39
|
unlike: (s: S) => Promise<S>;
|
|
33
40
|
setComment: (s: S, a?: unknown) => S;
|
|
@@ -35,12 +42,16 @@ const svc = defineService<S, {
|
|
|
35
42
|
/** [rico] #3372 — RichContent's on_link: `{ url }` of the tapped link. */
|
|
36
43
|
openLink: (s: S, a?: unknown) => Promise<S>;
|
|
37
44
|
}>("spacesForumPost", {
|
|
38
|
-
state:
|
|
45
|
+
state: INITIAL,
|
|
39
46
|
actions: {
|
|
40
|
-
|
|
47
|
+
prepare: (s) => ({ ...s, ...INITIAL }),
|
|
48
|
+
load: async (s): Promise<S | undefined> => {
|
|
49
|
+
const version = ++loadVersion;
|
|
50
|
+
await svc.prepare();
|
|
41
51
|
const id = detailInputId();
|
|
42
|
-
if (!id) return { ...
|
|
52
|
+
if (!id) return { ...INITIAL, status: "ready", ...presence("missing") };
|
|
43
53
|
try {
|
|
54
|
+
const access = await readAccess();
|
|
44
55
|
const res = await getPost(id);
|
|
45
56
|
const p = ((res as unknown as { post?: WixPost }).post) ?? (res as unknown as WixPost);
|
|
46
57
|
const pp = p as unknown as { title?: string; contentText?: string; content?: unknown; writer?: { name?: string }; commentCount?: number; viewCount?: number };
|
|
@@ -56,10 +67,15 @@ const svc = defineService<S, {
|
|
|
56
67
|
// which is the document's own text when there is no contentText.
|
|
57
68
|
const { bodyJson, bodyText } = richBody(doc, legacy);
|
|
58
69
|
const body = legacy || bodyText || "No content.";
|
|
59
|
-
|
|
70
|
+
await assertReadAccess(access);
|
|
71
|
+
if (version !== loadVersion) return undefined;
|
|
72
|
+
return { ...INITIAL, status: "ready", id, title: pp.title || "Post", author, subtitle: bits.join(" · "),
|
|
60
73
|
body, bodyJson, hasBodyJson: bodyJson !== "", initial: (author[0] || "#").toUpperCase(),
|
|
61
74
|
likeHint: "", commentText: "", commentHint: "", ...presence("found") };
|
|
62
|
-
} catch (e) {
|
|
75
|
+
} catch (e) {
|
|
76
|
+
const flags = await readSignInFlags(e);
|
|
77
|
+
return version === loadVersion ? { ...INITIAL, status: "ready", id, ...presenceAfterError(e), ...flags } : undefined;
|
|
78
|
+
}
|
|
63
79
|
},
|
|
64
80
|
// REAL WRITE: react to / un-react to the post.
|
|
65
81
|
like: async (s) => {
|
|
@@ -85,5 +101,6 @@ const svc = defineService<S, {
|
|
|
85
101
|
openLink: async (s, a) => ((await openRichLink(a)) ? s : { ...s, commentHint: "This link could not be opened." }),
|
|
86
102
|
},
|
|
87
103
|
});
|
|
104
|
+
svc.prepare();
|
|
88
105
|
svc.load();
|
|
89
106
|
export { svc };
|
|
@@ -14,7 +14,10 @@ import {
|
|
|
14
14
|
} from "@himalaya/wix-members";
|
|
15
15
|
import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
16
16
|
|
|
17
|
+
import { readAccess, assertReadAccess, readSignInFlags, NO_READ_SIGN_IN } from "../_shared/member-read.js";
|
|
18
|
+
|
|
17
19
|
interface S extends PresenceFlags {
|
|
20
|
+
signInRequired: boolean; noSignIn: boolean;
|
|
18
21
|
status: "loading" | "ready";
|
|
19
22
|
id: string;
|
|
20
23
|
name: string;
|
|
@@ -43,11 +46,7 @@ function followCountFor(followers: number, following: number): string {
|
|
|
43
46
|
return followers + " Followers · " + following + " Following";
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
const
|
|
47
|
-
load: (s: S) => Promise<S>;
|
|
48
|
-
toggleFollow: (s: S) => Promise<S>;
|
|
49
|
-
}>("spacesMemberDetail", {
|
|
50
|
-
state: {
|
|
49
|
+
const INITIAL: S = {
|
|
51
50
|
status: "loading",
|
|
52
51
|
id: "",
|
|
53
52
|
name: "",
|
|
@@ -61,13 +60,25 @@ const svc = defineService<S, {
|
|
|
61
60
|
following: 0,
|
|
62
61
|
followLabel: "Follow",
|
|
63
62
|
followCount: "0 Followers · 0 Following",
|
|
64
|
-
...presence("loading"),
|
|
65
|
-
}
|
|
63
|
+
...presence("loading"), ...NO_READ_SIGN_IN,
|
|
64
|
+
};
|
|
65
|
+
let loadVersion = 0;
|
|
66
|
+
|
|
67
|
+
const svc = defineService<S, {
|
|
68
|
+
prepare: (s: S) => S;
|
|
69
|
+
load: (s: S) => Promise<S | undefined>;
|
|
70
|
+
toggleFollow: (s: S) => Promise<S>;
|
|
71
|
+
}>("spacesMemberDetail", {
|
|
72
|
+
state: INITIAL,
|
|
66
73
|
actions: {
|
|
67
|
-
|
|
74
|
+
prepare: (s) => ({ ...s, ...INITIAL }),
|
|
75
|
+
load: async (s): Promise<S | undefined> => {
|
|
76
|
+
const version = ++loadVersion;
|
|
77
|
+
await svc.prepare();
|
|
68
78
|
const id = detailInputId();
|
|
69
|
-
if (!id) return { ...
|
|
79
|
+
if (!id) return { ...INITIAL, status: "ready", ...presence("missing") };
|
|
70
80
|
try {
|
|
81
|
+
const access = await readAccess();
|
|
71
82
|
const res = await getMember(id);
|
|
72
83
|
const m = ((res as unknown as { member?: WixMember }).member) ?? (res as unknown as WixMember);
|
|
73
84
|
const n = name(m);
|
|
@@ -87,8 +98,10 @@ const svc = defineService<S, {
|
|
|
87
98
|
isFollowing = await isFollowingMember(id);
|
|
88
99
|
} catch { /* default false */ }
|
|
89
100
|
|
|
101
|
+
await assertReadAccess(access);
|
|
102
|
+
if (version !== loadVersion) return undefined;
|
|
90
103
|
return {
|
|
91
|
-
status: "ready",
|
|
104
|
+
...INITIAL, status: "ready",
|
|
92
105
|
id,
|
|
93
106
|
name: n,
|
|
94
107
|
email: m.loginEmail || "",
|
|
@@ -104,7 +117,8 @@ const svc = defineService<S, {
|
|
|
104
117
|
...presence("found"),
|
|
105
118
|
};
|
|
106
119
|
} catch (e) {
|
|
107
|
-
|
|
120
|
+
const flags = await readSignInFlags(e);
|
|
121
|
+
return version === loadVersion ? { ...INITIAL, status: "ready", id, ...presenceAfterError(e), ...flags } : undefined;
|
|
108
122
|
}
|
|
109
123
|
},
|
|
110
124
|
// REAL WRITE: two-state toggle. Flips the relationship and optimistically
|
|
@@ -144,5 +158,6 @@ const svc = defineService<S, {
|
|
|
144
158
|
},
|
|
145
159
|
},
|
|
146
160
|
});
|
|
161
|
+
svc.prepare();
|
|
147
162
|
svc.load();
|
|
148
163
|
export { svc };
|
|
@@ -37,7 +37,7 @@ export default defineApp({
|
|
|
37
37
|
actions: "http://localhost:8190/v1/actions",
|
|
38
38
|
sync: "http://localhost:8190/v1/sync",
|
|
39
39
|
},
|
|
40
|
-
capabilities: ["
|
|
40
|
+
capabilities: ["push","share","secure_storage","open_url","calendar","local_database","wix_api","wix_platform","app_info","iap","release_control","clipboard","theme","wix_cashier"],
|
|
41
41
|
cache: {
|
|
42
42
|
"rules": [
|
|
43
43
|
{
|
|
@@ -11262,6 +11262,9 @@ const SCREENS = runtimeSetting({
|
|
|
11262
11262
|
"style": "body",
|
|
11263
11263
|
"color": "textSecondary"
|
|
11264
11264
|
}
|
|
11265
|
+
},
|
|
11266
|
+
"modifiers": {
|
|
11267
|
+
"hiddenPath": "$service:himiMyEvents.signInRequired"
|
|
11265
11268
|
}
|
|
11266
11269
|
},
|
|
11267
11270
|
{
|
|
@@ -11276,6 +11279,32 @@ const SCREENS = runtimeSetting({
|
|
|
11276
11279
|
"actionId": "tier5.himiMyEvents.load"
|
|
11277
11280
|
}
|
|
11278
11281
|
}
|
|
11282
|
+
},
|
|
11283
|
+
"modifiers": {
|
|
11284
|
+
"hiddenPath": "$service:himiMyEvents.signInRequired"
|
|
11285
|
+
}
|
|
11286
|
+
},
|
|
11287
|
+
{
|
|
11288
|
+
"id": "himi_my_events.retry.sign_in",
|
|
11289
|
+
"body": {
|
|
11290
|
+
"package": "himalaya",
|
|
11291
|
+
"type": "Button",
|
|
11292
|
+
"props": {
|
|
11293
|
+
"title": "Sign in",
|
|
11294
|
+
"style": "primary",
|
|
11295
|
+
"action": {
|
|
11296
|
+
"actionId": "himi.open",
|
|
11297
|
+
"params": {
|
|
11298
|
+
"screenId": "himi_sign_in",
|
|
11299
|
+
"mode": "signin",
|
|
11300
|
+
"modal": "1"
|
|
11301
|
+
},
|
|
11302
|
+
"presentation": "sheet"
|
|
11303
|
+
}
|
|
11304
|
+
}
|
|
11305
|
+
},
|
|
11306
|
+
"modifiers": {
|
|
11307
|
+
"hiddenPath": "$service:himiMyEvents.noSignIn"
|
|
11279
11308
|
}
|
|
11280
11309
|
}
|
|
11281
11310
|
]
|
|
@@ -11706,7 +11735,11 @@ const SCREENS = runtimeSetting({
|
|
|
11706
11735
|
"bottom": 24
|
|
11707
11736
|
},
|
|
11708
11737
|
"onAppear": {
|
|
11709
|
-
"actionId": "tier5.himiMyEvents.load"
|
|
11738
|
+
"actionId": "tier5.himiMyEvents.load",
|
|
11739
|
+
"params": {
|
|
11740
|
+
"signInScreenId": "himi_sign_in",
|
|
11741
|
+
"memberOnly": "1"
|
|
11742
|
+
}
|
|
11710
11743
|
}
|
|
11711
11744
|
}
|
|
11712
11745
|
},
|
|
@@ -23137,6 +23170,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
23137
23170
|
"style": "body",
|
|
23138
23171
|
"color": "textSecondary"
|
|
23139
23172
|
}
|
|
23173
|
+
},
|
|
23174
|
+
"modifiers": {
|
|
23175
|
+
"hiddenPath": "$service:himiBlogPost.signInRequired"
|
|
23140
23176
|
}
|
|
23141
23177
|
},
|
|
23142
23178
|
{
|
|
@@ -23151,6 +23187,32 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
23151
23187
|
"actionId": "tier5.himiBlogPost.retry"
|
|
23152
23188
|
}
|
|
23153
23189
|
}
|
|
23190
|
+
},
|
|
23191
|
+
"modifiers": {
|
|
23192
|
+
"hiddenPath": "$service:himiBlogPost.signInRequired"
|
|
23193
|
+
}
|
|
23194
|
+
},
|
|
23195
|
+
{
|
|
23196
|
+
"id": "himi_blog_post.retry.sign_in",
|
|
23197
|
+
"body": {
|
|
23198
|
+
"package": "himalaya",
|
|
23199
|
+
"type": "Button",
|
|
23200
|
+
"props": {
|
|
23201
|
+
"title": "Sign in",
|
|
23202
|
+
"style": "primary",
|
|
23203
|
+
"action": {
|
|
23204
|
+
"actionId": "himi.open",
|
|
23205
|
+
"params": {
|
|
23206
|
+
"screenId": "himi_sign_in",
|
|
23207
|
+
"mode": "signin",
|
|
23208
|
+
"modal": "1"
|
|
23209
|
+
},
|
|
23210
|
+
"presentation": "sheet"
|
|
23211
|
+
}
|
|
23212
|
+
}
|
|
23213
|
+
},
|
|
23214
|
+
"modifiers": {
|
|
23215
|
+
"hiddenPath": "$service:himiBlogPost.noSignIn"
|
|
23154
23216
|
}
|
|
23155
23217
|
}
|
|
23156
23218
|
]
|
|
@@ -23855,7 +23917,8 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
|
|
|
23855
23917
|
"onAppear": {
|
|
23856
23918
|
"actionId": "tier5.himiBlogPost.load",
|
|
23857
23919
|
"params": {
|
|
23858
|
-
"id": "__HIMI_ID__"
|
|
23920
|
+
"id": "__HIMI_ID__",
|
|
23921
|
+
"signInScreenId": "himi_sign_in"
|
|
23859
23922
|
}
|
|
23860
23923
|
}
|
|
23861
23924
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Load-state honesty for the branded-lite vertical workers.
|
|
2
|
+
//
|
|
3
|
+
// These workers used to answer every failed or empty query with representative SAMPLE
|
|
4
|
+
// rows — discount codes, gift-card balances, invoices, reservations, inbox messages —
|
|
5
|
+
// under a "Sample content" banner. On a customer build that is invented data presented
|
|
6
|
+
// as the customer's own: a code that does not exist, a balance in the wrong currency, an
|
|
7
|
+
// "Overdue" invoice, a booking labelled "tap to cancel". The banner did not make it
|
|
8
|
+
// honest; the rows were still the first thing a visitor read.
|
|
9
|
+
//
|
|
10
|
+
// The rule now:
|
|
11
|
+
// • query FAILED → status "error" with a customer sentence, a Retry, and — when
|
|
12
|
+
// Wix refused an anonymous visitor (401) — a Sign in path instead;
|
|
13
|
+
// • query was EMPTY → status "ready", no rows, the vertical's own empty sentence;
|
|
14
|
+
// • sample rows → ONLY when the host explicitly asked for sample data (the web
|
|
15
|
+
// preview's sample data mode, `__himiOwnerPreview: "sample"`).
|
|
16
|
+
// Never on a device, never as a fallback.
|
|
17
|
+
//
|
|
18
|
+
// Every field here is flat and boolean-gated because descriptors can only
|
|
19
|
+
// `hiddenWhen(<path>)` on a TRUE value: `notErrored` hides the error card, `noSignIn`
|
|
20
|
+
// hides the sign-in CTA, `signInRequired` hides Retry when retrying cannot help.
|
|
21
|
+
import { copyParam } from "./himi-copy.js";
|
|
22
|
+
|
|
23
|
+
export interface LoadFlags {
|
|
24
|
+
status: "loading" | "ready" | "error";
|
|
25
|
+
/** The customer-facing failure sentence; "" unless status is "error". */
|
|
26
|
+
error: string;
|
|
27
|
+
signInRequired: boolean;
|
|
28
|
+
notErrored: boolean;
|
|
29
|
+
/** hiddenWhen gate for a loading skeleton. */
|
|
30
|
+
notLoading: boolean;
|
|
31
|
+
/** Loading or failed — gates content that has nothing to say in either. */
|
|
32
|
+
busy: boolean;
|
|
33
|
+
noSignIn: boolean;
|
|
34
|
+
isMock: boolean;
|
|
35
|
+
notMock: boolean;
|
|
36
|
+
notMockEmpty: boolean;
|
|
37
|
+
notMockUnavailable: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const LOADING: LoadFlags = {
|
|
41
|
+
status: "loading", error: "", signInRequired: false, notErrored: true, notLoading: false, busy: true, noSignIn: true,
|
|
42
|
+
isMock: false, notMock: true, notMockEmpty: true, notMockUnavailable: true,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const READY: LoadFlags = { ...LOADING, status: "ready", notLoading: true, busy: false };
|
|
46
|
+
|
|
47
|
+
/** Did the HOST ask for sample data? Only the web preview's sample mode does; a native
|
|
48
|
+
* build, a visitor preview and a live owner preview never carry it. */
|
|
49
|
+
export function samplesRequested(): boolean {
|
|
50
|
+
const input = (globalThis as { himalaya?: { input?: Record<string, unknown> } }).himalaya?.input;
|
|
51
|
+
return input?.__himiOwnerPreview === "sample";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Wix refused an ANONYMOUS visitor: signing in is the fix, not retrying. A 403 is not
|
|
55
|
+
* this — it also answers "feature not installed" and a signed-in member without the
|
|
56
|
+
* permission, where a Sign in button leads nowhere — so it stays a Retry-able failure. */
|
|
57
|
+
export function needsSignIn(err: unknown): boolean {
|
|
58
|
+
const msg = String((err as { message?: unknown })?.message ?? err ?? "");
|
|
59
|
+
return /\b401\b|unauthori[sz]ed|unauthenticated/i.test(msg);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface LoadCopy<T> {
|
|
63
|
+
/** The copy namespace (`giftCards`, `coupons`, …) — keys resolve through copyParam. */
|
|
64
|
+
prefix: string;
|
|
65
|
+
/** Shown when the site genuinely has none. */
|
|
66
|
+
empty: string;
|
|
67
|
+
/** Shown when the query failed. */
|
|
68
|
+
error: string;
|
|
69
|
+
/** Shown when Wix refused the visitor (member-only data). */
|
|
70
|
+
signIn: string;
|
|
71
|
+
/** Sample rows for the explicit sample preview ONLY. */
|
|
72
|
+
sample: () => T[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type Rows<K extends string, T> = { [P in K]: T[] } & { emptyText: string } & LoadFlags;
|
|
76
|
+
|
|
77
|
+
/** A query answered. Rows win; an empty answer is a real empty state. */
|
|
78
|
+
export function loaded<K extends string, T>(key: K, rows: T[], copy: LoadCopy<T>): Rows<K, T> {
|
|
79
|
+
if (rows.length) return { ...READY, [key]: rows, emptyText: "" } as Rows<K, T>;
|
|
80
|
+
if (samplesRequested()) return sampleRows(key, copy, "empty");
|
|
81
|
+
return { ...READY, [key]: [], emptyText: copyParam(`${copy.prefix}.emptyText`, copy.empty) } as Rows<K, T>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A query threw. Never rows the customer did not create. */
|
|
85
|
+
export function loadFailed<K extends string, T>(key: K, err: unknown, copy: LoadCopy<T>): Rows<K, T> {
|
|
86
|
+
if (samplesRequested()) return sampleRows(key, copy, "unavailable");
|
|
87
|
+
const signIn = needsSignIn(err);
|
|
88
|
+
return {
|
|
89
|
+
...READY,
|
|
90
|
+
status: "error",
|
|
91
|
+
[key]: [],
|
|
92
|
+
emptyText: "",
|
|
93
|
+
error: signIn ? copyParam(`${copy.prefix}.signInText`, copy.signIn) : copyParam(`${copy.prefix}.errorText`, copy.error),
|
|
94
|
+
signInRequired: signIn,
|
|
95
|
+
notErrored: false,
|
|
96
|
+
busy: true,
|
|
97
|
+
noSignIn: !signIn,
|
|
98
|
+
} as Rows<K, T>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sampleRows<K extends string, T>(key: K, copy: LoadCopy<T>, reason: "empty" | "unavailable"): Rows<K, T> {
|
|
102
|
+
return {
|
|
103
|
+
...READY, [key]: copy.sample(), emptyText: "",
|
|
104
|
+
isMock: true, notMock: false, notMockEmpty: reason !== "empty", notMockUnavailable: reason !== "unavailable",
|
|
105
|
+
} as Rows<K, T>;
|
|
106
|
+
}
|