@wix/himalaya-cli 0.1569.0 → 0.1571.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/l10n-strings.d.ts +3 -2
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/define-app.d.ts +8 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/auth-providers/wix/src/index.d.ts +7 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/screens/src/business-contact-service.d.ts +17 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/screens/src/business-contact.d.ts +73 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/screens/src/index.d.ts +3 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/screens/src/invite-target.d.ts +1 -0
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/screens/src/invite.d.ts +148 -0
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/blog-v3/src/extensions.d.ts +6 -3
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/auth.mjs +17 -7
  11. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core/validation.mjs +17 -7
  12. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +17 -7
  13. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +575 -91
  14. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/state.mjs +17 -7
  15. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-auth.mjs +7 -1
  16. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-blog.mjs +1 -0
  17. package/dist/cli.mjs +1897 -1226
  18. package/dist/preview-assets.generated.bin +0 -0
  19. package/guides/base44-app-to-himi-app.md +1 -1
  20. package/guides/capabilities.md +7 -0
  21. package/guides/getting-started.md +5 -0
  22. package/guides/himi-json-output.md +11 -0
  23. package/guides/himi-skill.md +5 -0
  24. package/guides/one-app-to-himi-app.md +267 -8
  25. package/guides/sdk-reference.json +2 -2
  26. package/guides/skill.md +5 -0
  27. package/guides/splash-screens.md +10 -1
  28. package/guides/tier-5-add-a-screen.md +20 -0
  29. package/guides/trim-a-template.md +5 -0
  30. package/guides/wix-business-context.md +11 -2
  31. package/package.json +2 -2
  32. package/templates/beauty-salon/config.ts +8 -1
  33. package/templates/beauty-salon/dev/index.ts +244 -1
  34. package/templates/beauty-salon/himalaya.content.json +3 -0
  35. package/templates/beauty-salon/tier5-src/_shared/member-read.ts +52 -0
  36. package/templates/beauty-salon/tier5-src/_shared/program-description.ts +11 -0
  37. package/templates/beauty-salon/tier5-src/business-contact/index.ts +6 -0
  38. package/templates/beauty-salon/tier5-src/himi-blog-post/index.ts +35 -12
  39. package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +3 -3
  40. package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +6 -9
  41. package/templates/beauty-salon-owner/config.ts +1 -1
  42. package/templates/branded-lite/config.ts +8 -1
  43. package/templates/branded-lite/dev/index.ts +409 -2
  44. package/templates/branded-lite/himalaya.content.json +3 -0
  45. package/templates/branded-lite/tier5-src/_shared/member-read.ts +52 -0
  46. package/templates/branded-lite/tier5-src/_shared/program-description.ts +11 -0
  47. package/templates/branded-lite/tier5-src/business-contact/index.ts +6 -0
  48. package/templates/branded-lite/tier5-src/himi-blog-post/index.ts +35 -12
  49. package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +3 -3
  50. package/templates/branded-lite/tier5-src/himi-branded-lite-notifsettings/index.ts +39 -11
  51. package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +16 -14
  52. package/templates/branded-lite/tier5-src/himi-branded-lite-programs/index.ts +3 -7
  53. package/templates/branded-lite/tier5-src/himi-events-my-events/index.ts +76 -42
  54. package/templates/branded-lite/tier5-src/himi-store-product/index.ts +6 -9
  55. package/templates/branded-lite/tier5-src/wix-spaces-chat-thread/index.ts +101 -34
  56. package/templates/branded-lite/tier5-src/wix-spaces-forum-post/index.ts +23 -6
  57. package/templates/branded-lite/tier5-src/wix-spaces-member-detail/index.ts +26 -11
  58. package/templates/community-club/config.ts +8 -1
  59. package/templates/community-club/dev/index.ts +278 -2
  60. package/templates/community-club/himalaya.content.json +3 -0
  61. package/templates/community-club/tier5-src/_shared/branded-load-state.ts +106 -0
  62. package/templates/community-club/tier5-src/_shared/member-read.ts +52 -0
  63. package/templates/community-club/tier5-src/_shared/program-description.ts +11 -0
  64. package/templates/community-club/tier5-src/business-contact/index.ts +6 -0
  65. package/templates/community-club/tier5-src/himi-blog-post/index.ts +35 -12
  66. package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +3 -3
  67. package/templates/community-club/tier5-src/himi-events-my-events/index.ts +76 -42
  68. package/templates/community-club-owner/config.ts +1 -1
  69. package/templates/events-meetups/config.ts +8 -1
  70. package/templates/events-meetups/dev/index.ts +278 -2
  71. package/templates/events-meetups/himalaya.content.json +3 -0
  72. package/templates/events-meetups/tier5-src/_shared/branded-load-state.ts +106 -0
  73. package/templates/events-meetups/tier5-src/_shared/member-read.ts +52 -0
  74. package/templates/events-meetups/tier5-src/_shared/program-description.ts +11 -0
  75. package/templates/events-meetups/tier5-src/business-contact/index.ts +6 -0
  76. package/templates/events-meetups/tier5-src/himi-blog-post/index.ts +35 -12
  77. package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +3 -3
  78. package/templates/events-meetups/tier5-src/himi-events-my-events/index.ts +76 -42
  79. package/templates/events-meetups-owner/config.ts +1 -1
  80. package/templates/gym-fitness/config.ts +8 -1
  81. package/templates/gym-fitness/dev/index.ts +278 -2
  82. package/templates/gym-fitness/himalaya.content.json +3 -0
  83. package/templates/gym-fitness/tier5-src/_shared/member-read.ts +52 -0
  84. package/templates/gym-fitness/tier5-src/_shared/program-description.ts +11 -0
  85. package/templates/gym-fitness/tier5-src/business-contact/index.ts +6 -0
  86. package/templates/gym-fitness/tier5-src/himi-blog-post/index.ts +35 -12
  87. package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +3 -3
  88. package/templates/gym-fitness/tier5-src/himi-events-my-events/index.ts +76 -42
  89. package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +6 -9
  90. package/templates/gym-fitness-owner/config.ts +1 -1
  91. package/templates/nonprofit-cause/config.ts +8 -1
  92. package/templates/nonprofit-cause/dev/index.ts +278 -2
  93. package/templates/nonprofit-cause/himalaya.content.json +3 -0
  94. package/templates/nonprofit-cause/tier5-src/_shared/branded-load-state.ts +106 -0
  95. package/templates/nonprofit-cause/tier5-src/_shared/member-read.ts +52 -0
  96. package/templates/nonprofit-cause/tier5-src/_shared/program-description.ts +11 -0
  97. package/templates/nonprofit-cause/tier5-src/business-contact/index.ts +6 -0
  98. package/templates/nonprofit-cause/tier5-src/himi-blog-post/index.ts +35 -12
  99. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +3 -3
  100. package/templates/nonprofit-cause/tier5-src/himi-events-my-events/index.ts +76 -42
  101. package/templates/nonprofit-cause-owner/config.ts +1 -1
  102. package/templates/owner-lite/config.ts +1 -1
  103. package/templates/podcast-creator/config.ts +8 -1
  104. package/templates/podcast-creator/dev/index.ts +278 -2
  105. package/templates/podcast-creator/himalaya.content.json +3 -0
  106. package/templates/podcast-creator/tier5-src/_shared/branded-load-state.ts +106 -0
  107. package/templates/podcast-creator/tier5-src/_shared/member-read.ts +52 -0
  108. package/templates/podcast-creator/tier5-src/_shared/program-description.ts +11 -0
  109. package/templates/podcast-creator/tier5-src/business-contact/index.ts +6 -0
  110. package/templates/podcast-creator/tier5-src/himi-blog-post/index.ts +35 -12
  111. package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +3 -3
  112. package/templates/podcast-creator/tier5-src/himi-events-my-events/index.ts +76 -42
  113. package/templates/podcast-creator-owner/config.ts +1 -1
  114. package/templates/portfolio-creative/config.ts +8 -1
  115. package/templates/portfolio-creative/dev/index.ts +278 -2
  116. package/templates/portfolio-creative/himalaya.content.json +3 -0
  117. package/templates/portfolio-creative/tier5-src/_shared/branded-load-state.ts +106 -0
  118. package/templates/portfolio-creative/tier5-src/_shared/member-read.ts +52 -0
  119. package/templates/portfolio-creative/tier5-src/_shared/program-description.ts +11 -0
  120. package/templates/portfolio-creative/tier5-src/business-contact/index.ts +6 -0
  121. package/templates/portfolio-creative/tier5-src/himi-blog-post/index.ts +35 -12
  122. package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +3 -3
  123. package/templates/portfolio-creative/tier5-src/himi-events-my-events/index.ts +76 -42
  124. package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +6 -9
  125. package/templates/portfolio-creative-owner/config.ts +1 -1
  126. package/templates/restaurant/config.ts +8 -1
  127. package/templates/restaurant/dev/index.ts +278 -2
  128. package/templates/restaurant/himalaya.content.json +3 -0
  129. package/templates/restaurant/tier5-src/_shared/branded-load-state.ts +106 -0
  130. package/templates/restaurant/tier5-src/_shared/member-read.ts +52 -0
  131. package/templates/restaurant/tier5-src/_shared/program-description.ts +11 -0
  132. package/templates/restaurant/tier5-src/business-contact/index.ts +6 -0
  133. package/templates/restaurant/tier5-src/himi-blog-post/index.ts +35 -12
  134. package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +3 -3
  135. package/templates/restaurant/tier5-src/himi-events-my-events/index.ts +76 -42
  136. package/templates/restaurant-owner/config.ts +1 -1
  137. package/templates/retail-store/config.ts +8 -1
  138. package/templates/retail-store/dev/index.ts +244 -1
  139. package/templates/retail-store/himalaya.content.json +3 -0
  140. package/templates/retail-store/tier5-src/_shared/branded-load-state.ts +106 -0
  141. package/templates/retail-store/tier5-src/_shared/member-read.ts +52 -0
  142. package/templates/retail-store/tier5-src/_shared/program-description.ts +11 -0
  143. package/templates/retail-store/tier5-src/business-contact/index.ts +6 -0
  144. package/templates/retail-store/tier5-src/himi-blog-post/index.ts +35 -12
  145. package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +3 -3
  146. package/templates/retail-store/tier5-src/himi-store-product/index.ts +6 -9
  147. package/templates/retail-store-owner/config.ts +1 -1
  148. package/templates/yoga-studio/config.ts +8 -1
  149. package/templates/yoga-studio/dev/index.ts +306 -2
  150. package/templates/yoga-studio/himalaya.content.json +3 -0
  151. package/templates/yoga-studio/tier5-src/_shared/member-read.ts +52 -0
  152. package/templates/yoga-studio/tier5-src/_shared/program-description.ts +11 -0
  153. package/templates/yoga-studio/tier5-src/business-contact/index.ts +6 -0
  154. package/templates/yoga-studio/tier5-src/himi-blog-post/index.ts +35 -12
  155. package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +3 -3
  156. package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +16 -14
  157. package/templates/yoga-studio/tier5-src/himi-branded-lite-programs/index.ts +3 -7
  158. package/templates/yoga-studio/tier5-src/himi-events-my-events/index.ts +76 -42
  159. package/templates/yoga-studio-owner/config.ts +1 -1
@@ -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
- const token = memberToken.get();
131
- try {
132
- const msgs = await getMessages(id, 30, { role: "member", chatToken: await token });
133
- return rowsOf(msgs ?? [], myUserId);
134
- } catch (e) {
135
- memberToken.drop(token);
136
- throw e;
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) => sendChatMessage(chatroomId, text, { role: "member", chatToken, messageId }));
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
- return svc.getState();
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
- load: (s: S, a?: unknown) => Promise<S>;
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: { status: "loading", messages: [], emptyText: "", emptyHidden: true, scrollSend: "", draft: "", sendHint: "", resolvedChatroomId: "", myUserId: "", ...presence("loading") },
234
+ state: INITIAL,
199
235
  actions: {
200
- load: async (s, a) => {
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 { ...s, status: "ready", messages: [], emptyText: "", emptyHidden: true, ...presence("missing") };
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 messages = await reload(chatroomId, myUserId);
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
- return { ...s, 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) };
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
- return { ...s, status: "ready", resolvedChatroomId: chatroomId, myUserId, messages: [], emptyText: "", emptyHidden: true, ...presenceAfterError(e) };
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
- if (!text) return { ...s, sendHint: "Type a message first." };
239
- if (sendingNow) return s;
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
- load: (s: S) => Promise<S>;
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: { status: "loading", id: "", title: "", author: "", subtitle: "", body: "", bodyJson: "", hasBodyJson: false, initial: "", likeHint: "", commentText: "", commentHint: "", ...presence("loading") },
45
+ state: INITIAL,
39
46
  actions: {
40
- load: async (s) => {
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 { ...s, status: "ready", ...presence("missing") };
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
- return { status: "ready", id, title: pp.title || "Post", author, subtitle: bits.join(" · "),
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) { return { ...s, status: "ready", id, ...presenceAfterError(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 svc = defineService<S, {
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
- load: async (s) => {
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 { ...s, status: "ready", ...presence("missing") };
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
- return { ...s, status: "ready", id, ...presenceAfterError(e) };
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: ["live_activity","push","share","secure_storage","open_url","calendar","local_database","wix_api","wix_platform","app_info","iap","release_control","clipboard","theme"],
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
  {
@@ -81,6 +81,13 @@ export default defineApp({
81
81
  "parentScreenId": "himi_account",
82
82
  "tabId": "profile"
83
83
  },
84
+ {
85
+ "screenId": "himi_business_contact",
86
+ "isPublic": true,
87
+ "paramKeys": [],
88
+ "parentScreenId": "himi_account",
89
+ "tabId": "profile"
90
+ },
84
91
  {
85
92
  "screenId": "himi_appearance",
86
93
  "isPublic": true,