@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.
Files changed (142) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/define-app.d.ts +8 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/auth-providers/wix/src/index.d.ts +7 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/blog-v3/src/extensions.d.ts +6 -3
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/members-feed-viewer/src/index.d.ts +6 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/auth.mjs +17 -7
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core/validation.mjs +17 -7
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +17 -7
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +17 -7
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/state.mjs +17 -7
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-auth.mjs +7 -1
  11. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-blog.mjs +1 -0
  12. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-members-feed-viewer.mjs +18 -0
  13. package/dist/cli.mjs +2014 -1252
  14. package/dist/preview-assets.generated.bin +0 -0
  15. package/guides/base44-app-to-himi-app.md +1 -1
  16. package/guides/capabilities.md +7 -0
  17. package/guides/himi-json-output.md +11 -0
  18. package/guides/index.json +7 -0
  19. package/guides/map-one-app-coverage.js +117 -0
  20. package/guides/one-app-to-himi-app.md +209 -11
  21. package/guides/recipe-one-app-coverage-mapping.md +74 -0
  22. package/guides/sdk-reference.json +8 -2
  23. package/guides/splash-screens.md +10 -1
  24. package/guides/trim-a-template.md +5 -0
  25. package/guides/wix-business-context.md +11 -2
  26. package/package.json +2 -2
  27. package/templates/beauty-salon/config.ts +1 -1
  28. package/templates/beauty-salon/dev/index.ts +31 -1
  29. package/templates/beauty-salon/tier5-src/_shared/member-read.ts +52 -0
  30. package/templates/beauty-salon/tier5-src/_shared/program-description.ts +11 -0
  31. package/templates/beauty-salon/tier5-src/himi-blog-post/index.ts +35 -12
  32. package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +3 -3
  33. package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +6 -9
  34. package/templates/beauty-salon-owner/config.ts +1 -1
  35. package/templates/beauty-salon-owner/dev/index.ts +2 -3
  36. package/templates/branded-lite/config.ts +1 -1
  37. package/templates/branded-lite/dev/index.ts +163 -2
  38. package/templates/branded-lite/tier5-src/_shared/member-read.ts +52 -0
  39. package/templates/branded-lite/tier5-src/_shared/program-description.ts +11 -0
  40. package/templates/branded-lite/tier5-src/himi-blog-post/index.ts +35 -12
  41. package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +3 -3
  42. package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +16 -14
  43. package/templates/branded-lite/tier5-src/himi-branded-lite-programs/index.ts +31 -18
  44. package/templates/branded-lite/tier5-src/himi-events-my-events/index.ts +76 -42
  45. package/templates/branded-lite/tier5-src/himi-store-product/index.ts +6 -9
  46. package/templates/branded-lite/tier5-src/wix-spaces-chat-thread/index.ts +101 -34
  47. package/templates/branded-lite/tier5-src/wix-spaces-forum-post/index.ts +23 -6
  48. package/templates/branded-lite/tier5-src/wix-spaces-member-detail/index.ts +26 -11
  49. package/templates/community-club/config.ts +1 -1
  50. package/templates/community-club/dev/index.ts +65 -2
  51. package/templates/community-club/tier5-src/_shared/branded-load-state.ts +106 -0
  52. package/templates/community-club/tier5-src/_shared/member-read.ts +52 -0
  53. package/templates/community-club/tier5-src/_shared/program-description.ts +11 -0
  54. package/templates/community-club/tier5-src/himi-blog-post/index.ts +35 -12
  55. package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +3 -3
  56. package/templates/community-club/tier5-src/himi-events-my-events/index.ts +76 -42
  57. package/templates/community-club-owner/config.ts +1 -1
  58. package/templates/community-club-owner/dev/index.ts +2 -3
  59. package/templates/events-meetups/config.ts +1 -1
  60. package/templates/events-meetups/dev/index.ts +65 -2
  61. package/templates/events-meetups/tier5-src/_shared/branded-load-state.ts +106 -0
  62. package/templates/events-meetups/tier5-src/_shared/member-read.ts +52 -0
  63. package/templates/events-meetups/tier5-src/_shared/program-description.ts +11 -0
  64. package/templates/events-meetups/tier5-src/himi-blog-post/index.ts +35 -12
  65. package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +3 -3
  66. package/templates/events-meetups/tier5-src/himi-events-my-events/index.ts +76 -42
  67. package/templates/events-meetups-owner/config.ts +1 -1
  68. package/templates/events-meetups-owner/dev/index.ts +2 -3
  69. package/templates/gym-fitness/config.ts +1 -1
  70. package/templates/gym-fitness/dev/index.ts +65 -2
  71. package/templates/gym-fitness/tier5-src/_shared/member-read.ts +52 -0
  72. package/templates/gym-fitness/tier5-src/_shared/program-description.ts +11 -0
  73. package/templates/gym-fitness/tier5-src/himi-blog-post/index.ts +35 -12
  74. package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +3 -3
  75. package/templates/gym-fitness/tier5-src/himi-events-my-events/index.ts +76 -42
  76. package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +6 -9
  77. package/templates/gym-fitness-owner/config.ts +1 -1
  78. package/templates/gym-fitness-owner/dev/index.ts +2 -3
  79. package/templates/nonprofit-cause/config.ts +1 -1
  80. package/templates/nonprofit-cause/dev/index.ts +65 -2
  81. package/templates/nonprofit-cause/tier5-src/_shared/branded-load-state.ts +106 -0
  82. package/templates/nonprofit-cause/tier5-src/_shared/member-read.ts +52 -0
  83. package/templates/nonprofit-cause/tier5-src/_shared/program-description.ts +11 -0
  84. package/templates/nonprofit-cause/tier5-src/himi-blog-post/index.ts +35 -12
  85. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +3 -3
  86. package/templates/nonprofit-cause/tier5-src/himi-events-my-events/index.ts +76 -42
  87. package/templates/nonprofit-cause-owner/config.ts +1 -1
  88. package/templates/nonprofit-cause-owner/dev/index.ts +2 -3
  89. package/templates/owner-lite/config.ts +1 -1
  90. package/templates/owner-lite/dev/index.ts +2 -3
  91. package/templates/podcast-creator/config.ts +1 -1
  92. package/templates/podcast-creator/dev/index.ts +65 -2
  93. package/templates/podcast-creator/tier5-src/_shared/branded-load-state.ts +106 -0
  94. package/templates/podcast-creator/tier5-src/_shared/member-read.ts +52 -0
  95. package/templates/podcast-creator/tier5-src/_shared/program-description.ts +11 -0
  96. package/templates/podcast-creator/tier5-src/himi-blog-post/index.ts +35 -12
  97. package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +3 -3
  98. package/templates/podcast-creator/tier5-src/himi-events-my-events/index.ts +76 -42
  99. package/templates/podcast-creator-owner/config.ts +1 -1
  100. package/templates/podcast-creator-owner/dev/index.ts +2 -3
  101. package/templates/portfolio-creative/config.ts +1 -1
  102. package/templates/portfolio-creative/dev/index.ts +65 -2
  103. package/templates/portfolio-creative/tier5-src/_shared/branded-load-state.ts +106 -0
  104. package/templates/portfolio-creative/tier5-src/_shared/member-read.ts +52 -0
  105. package/templates/portfolio-creative/tier5-src/_shared/program-description.ts +11 -0
  106. package/templates/portfolio-creative/tier5-src/himi-blog-post/index.ts +35 -12
  107. package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +3 -3
  108. package/templates/portfolio-creative/tier5-src/himi-events-my-events/index.ts +76 -42
  109. package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +6 -9
  110. package/templates/portfolio-creative-owner/config.ts +1 -1
  111. package/templates/portfolio-creative-owner/dev/index.ts +2 -3
  112. package/templates/restaurant/config.ts +1 -1
  113. package/templates/restaurant/dev/index.ts +65 -2
  114. package/templates/restaurant/tier5-src/_shared/branded-load-state.ts +106 -0
  115. package/templates/restaurant/tier5-src/_shared/member-read.ts +52 -0
  116. package/templates/restaurant/tier5-src/_shared/program-description.ts +11 -0
  117. package/templates/restaurant/tier5-src/himi-blog-post/index.ts +35 -12
  118. package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +3 -3
  119. package/templates/restaurant/tier5-src/himi-events-my-events/index.ts +76 -42
  120. package/templates/restaurant-owner/config.ts +1 -1
  121. package/templates/restaurant-owner/dev/index.ts +2 -3
  122. package/templates/retail-store/config.ts +1 -1
  123. package/templates/retail-store/dev/index.ts +31 -1
  124. package/templates/retail-store/tier5-src/_shared/branded-load-state.ts +106 -0
  125. package/templates/retail-store/tier5-src/_shared/member-read.ts +52 -0
  126. package/templates/retail-store/tier5-src/_shared/program-description.ts +11 -0
  127. package/templates/retail-store/tier5-src/himi-blog-post/index.ts +35 -12
  128. package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +3 -3
  129. package/templates/retail-store/tier5-src/himi-store-product/index.ts +6 -9
  130. package/templates/retail-store-owner/config.ts +1 -1
  131. package/templates/retail-store-owner/dev/index.ts +2 -3
  132. package/templates/yoga-studio/config.ts +1 -1
  133. package/templates/yoga-studio/dev/index.ts +93 -2
  134. package/templates/yoga-studio/tier5-src/_shared/member-read.ts +52 -0
  135. package/templates/yoga-studio/tier5-src/_shared/program-description.ts +11 -0
  136. package/templates/yoga-studio/tier5-src/himi-blog-post/index.ts +35 -12
  137. package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +3 -3
  138. package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +16 -14
  139. package/templates/yoga-studio/tier5-src/himi-branded-lite-programs/index.ts +31 -18
  140. package/templates/yoga-studio/tier5-src/himi-events-my-events/index.ts +76 -42
  141. package/templates/yoga-studio-owner/config.ts +1 -1
  142. package/templates/yoga-studio-owner/dev/index.ts +2 -3
@@ -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
+ }
@@ -0,0 +1,52 @@
1
+ import { memberAccess, memberAccessChanged, memberAccessDenied, type MemberAccess } from "./member-access.js";
2
+ import { needsSignIn } from "./branded-load-state.js";
3
+
4
+ /** Public reads stay public. Capture their reader only to reject a result that
5
+ * belongs to the identity/site which was active before the request. */
6
+ export interface ReadAccess { member: MemberAccess; tenant: string; }
7
+ export async function readAccess(): Promise<ReadAccess> {
8
+ const member = await memberAccess();
9
+ const input = (globalThis as { himalaya?: { input?: Record<string, unknown> } }).himalaya?.input;
10
+ return { member, tenant: JSON.stringify([input?.__himiPreferenceAccount ?? "", input?.__himiPreferenceBusiness ?? ""]) };
11
+ }
12
+
13
+ export async function assertReadAccess(access: ReadAccess): Promise<void> {
14
+ const current = await readAccess();
15
+ if (current.member.email !== access.member.email || current.member.scope !== access.member.scope || current.tenant !== access.tenant) throw memberAccessChanged();
16
+ }
17
+
18
+ /** Bridge errors put the actual status first; codes quoted inside the body are data. */
19
+ function httpStatus(error: unknown): string | undefined {
20
+ return /^HTTP\s*(\d{3})\b/i.exec(String((error as { message?: unknown })?.message ?? error))?.[1];
21
+ }
22
+
23
+ export const NO_READ_SIGN_IN = { signInRequired: false, noSignIn: true };
24
+
25
+ /** An authorization refusal must not fall back to a previously cached article. */
26
+ export function readAccessDenied(error: unknown): boolean {
27
+ let cause = error;
28
+ for (let depth = 0; cause && depth < 4; depth++) {
29
+ const status = httpStatus(cause);
30
+ if (status ? status === "401" || status === "403" : memberAccessDenied(cause) || needsSignIn(cause)) return true;
31
+ cause = (cause as { cause?: unknown }).cause;
32
+ }
33
+ return false;
34
+ }
35
+
36
+ /** A permission failure alone says nothing about whether signing in can help.
37
+ * In particular, a private member profile can be unavailable to members too.
38
+ * Only an authentication refusal for a current visitor offers Sign in. Follow
39
+ * SDK wrapper causes (blog getPost preserves its original read error there). */
40
+ export async function readSignInFlags(error: unknown): Promise<typeof NO_READ_SIGN_IN> {
41
+ let authenticationRequired = false;
42
+ let permissionRefused = false;
43
+ let cause = error;
44
+ for (let depth = 0; cause && depth < 4; depth++) {
45
+ const status = httpStatus(cause);
46
+ permissionRefused ||= status === "403";
47
+ authenticationRequired ||= status ? status === "401" : needsSignIn(cause);
48
+ cause = (cause as { cause?: unknown }).cause;
49
+ }
50
+ const signInRequired = !permissionRefused && authenticationRequired && !(await memberAccess()).email;
51
+ return { signInRequired, noSignIn: !signInRequired };
52
+ }
@@ -0,0 +1,11 @@
1
+ import type { WixProgram } from "@himalaya/wix-online-programs";
2
+
3
+ /** v3 nests the body in description.details; older responses use settings or a
4
+ * flat description. The body itself may be a Ricos document or serialized text. */
5
+ export function programDescription(program: WixProgram): unknown {
6
+ const raw = (program as { description?: unknown }).description;
7
+ const nested = raw && typeof raw === "object" ? raw as { details?: unknown; nodes?: unknown } : undefined;
8
+ return program.settings?.description ?? nested?.details
9
+ ?? (typeof raw === "string" || Array.isArray(nested?.nodes) ? raw : undefined)
10
+ ?? program.details?.description;
11
+ }
@@ -9,6 +9,7 @@ import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
9
9
  import { formatDateMedium, formatMonthDay } from "../_shared/engine-intl.js";
10
10
  import { openRichLink } from "../_shared/rich-content-link.js";
11
11
  import { richBody, ROW_IMAGE_WIDTH } from "../_shared/rich-body.js";
12
+ import { readAccess, assertReadAccess, readAccessDenied, readSignInFlags, NO_READ_SIGN_IN } from "../_shared/member-read.js";
12
13
 
13
14
  // Offline-first (data-offline): the post body is a public read-through — when the
14
15
  // live fetch fails (offline) fall back to the last-synced row the feed cached under
@@ -34,6 +35,7 @@ interface CommentRow {
34
35
  }
35
36
 
36
37
  interface S {
38
+ signInRequired: boolean; noSignIn: boolean; signInScreenId: string;
37
39
  status: "loading" | "ready" | "error";
38
40
  id: string;
39
41
  title: string;
@@ -214,6 +216,7 @@ function derive(s: S): S {
214
216
  }
215
217
 
216
218
  const baseState: S = derive({
219
+ ...NO_READ_SIGN_IN, signInScreenId: "himi_account",
217
220
  status: "loading",
218
221
  id: "",
219
222
  title: "",
@@ -280,23 +283,28 @@ async function cachedPostState(s: S, id: string): Promise<S> {
280
283
  });
281
284
  }
282
285
 
283
- async function loadPostState(s: S): Promise<S> {
286
+ let loadVersion = 0;
287
+ async function loadPostState(s: S): Promise<S | undefined> {
288
+ const version = ++loadVersion;
289
+ await svc.prepare();
290
+ const clean = { ...baseState, signInScreenId: s.signInScreenId };
284
291
  const id = inputId();
285
292
  if (!id) {
286
- return derive({ ...s, status: "ready", emptyText: copyParam("blogPost.notFoundText", "Post not found.") });
293
+ return derive({ ...clean, status: "ready", emptyText: copyParam("blogPost.notFoundText", "Post not found.") });
287
294
  }
288
295
  try {
289
- const [postResult, commentsResult] = await Promise.allSettled([getPost(id), listBlogComments(id)]);
290
- if (postResult.status === "rejected") {
291
- return cachedPostState(s, id);
292
- }
296
+ const access = await readAccess();
297
+ const [postResult, commentsResult] = await Promise.allSettled([getPost(id, {}, { preserveAccessErrors: true }), listBlogComments(id)]);
298
+ if (postResult.status === "rejected") throw postResult.reason;
299
+ await assertReadAccess(access);
300
+ if (version !== loadVersion) return undefined;
293
301
  const post = (((postResult.value as unknown as { post?: WixPost }).post) ?? postResult.value) as PostWithExtras;
294
302
  const commentsUnavailable = commentsResult.status === "rejected";
295
303
  const comments = commentsResult.status === "fulfilled" ? commentsResult.value.map(toCommentRow) : [];
296
304
  const cover = imageUrl(post);
297
305
  const title = post.title || "Untitled post";
298
306
  return derive({
299
- ...s,
307
+ ...clean,
300
308
  status: "ready",
301
309
  id,
302
310
  title,
@@ -319,14 +327,23 @@ async function loadPostState(s: S): Promise<S> {
319
327
  errorText: "",
320
328
  emptyText: "",
321
329
  });
322
- } catch {
323
- return cachedPostState(s, id);
330
+ } catch (error) {
331
+ const flags = await readSignInFlags(error);
332
+ const failed = derive({ ...clean, ...flags, status: "error", id, errorText: copyParam("blogPost.errorText", "This post could not load.") });
333
+ if (version !== loadVersion) return undefined;
334
+ if (readAccessDenied(error)) return failed;
335
+ const access = await readAccess();
336
+ const cached = await cachedPostState(clean, id);
337
+ try { await assertReadAccess(access); }
338
+ catch { return version === loadVersion ? failed : undefined; }
339
+ return version === loadVersion ? cached : undefined;
324
340
  }
325
341
  }
326
342
 
327
343
  const svc = defineService<S, {
328
- load: (s: S, arg?: unknown) => Promise<S>;
329
- retry: (s: S) => Promise<S>;
344
+ prepare: (s: S) => S;
345
+ load: (s: S, arg?: unknown) => Promise<S | undefined>;
346
+ retry: (s: S) => Promise<S | undefined>;
330
347
  like: (s: S) => Promise<S>;
331
348
  unlike: (s: S) => Promise<S>;
332
349
  toggleSave: (s: S) => S;
@@ -338,7 +355,12 @@ const svc = defineService<S, {
338
355
  }>("himiBlogPost", {
339
356
  state: baseState,
340
357
  actions: {
341
- load: (s, arg) => { stashCopy(arg); return loadPostState(s); },
358
+ prepare: (s) => ({ ...s, ...baseState }),
359
+ load: (s, arg) => {
360
+ stashCopy(arg);
361
+ const target = (arg as { signInScreenId?: unknown })?.signInScreenId;
362
+ return loadPostState({ ...s, signInScreenId: target === "himi_sign_in" ? target : s.signInScreenId });
363
+ },
342
364
  retry: (s) => loadPostState({ ...s, status: "loading", errorText: "" }),
343
365
  like: async (s) => {
344
366
  if (!s.id) return derive({ ...s, actionMessage: copyParam("blogPost.noPostMessage", "No post selected.") });
@@ -409,5 +431,6 @@ const svc = defineService<S, {
409
431
  },
410
432
  });
411
433
 
434
+ svc.prepare();
412
435
  svc.load();
413
436
  export { svc };
@@ -1,3 +1,5 @@
1
+ import { descriptionBody } from "../_shared/rich-body.js";
2
+ import { programDescription } from "../_shared/program-description.js";
1
3
  import { fetchMemberBookings, type MemberBookingRow } from "../_shared/member-bookings.js";
2
4
  import { hostSiteFromInput, hostedExploreCards, hostedHiddenFlags, type HostedExploreCard } from "../_shared/branded-lite-hosted.js";
3
5
  import { memberAccess, assertMemberAccess } from "../_shared/member-access.js";
@@ -675,11 +677,9 @@ async function fetchHomePrograms(): Promise<ProgramCard[]> {
675
677
  .map((p: WixProgram) => {
676
678
  const desc = (p as { description?: unknown }).description;
677
679
  const nestedTitle = desc && typeof desc === "object" ? (desc as { title?: string }).title : undefined;
678
- const nestedDetails = desc && typeof desc === "object" ? (desc as { details?: string }).details : undefined;
679
680
  const name = cleanTitle(p.settings?.title ?? nestedTitle ?? p.title) || copyParam("home.programFallbackName", "Program");
680
681
  const image = programImage(p);
681
- const details = (p.settings?.description ?? nestedDetails ?? (typeof desc === "string" ? desc : "") ?? "")
682
- .replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
682
+ const details = descriptionBody(programDescription(p)).bodyText.replace(/\s+/g, " ").trim();
683
683
  return {
684
684
  id: p.id || name,
685
685
  name,
@@ -19,7 +19,7 @@ import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
19
19
  import { isBridgeUnavailable } from "../_shared/owner-load-error.js";
20
20
  import { isProductSaved, toggleSavedProduct } from "../_shared/himi-store-saved.js";
21
21
  import { openRichLink } from "../_shared/rich-content-link.js";
22
- import { richBody } from "../_shared/rich-body.js";
22
+ import { descriptionBody } from "../_shared/rich-body.js";
23
23
 
24
24
  /** Live cart size (sum of line-item quantities) for the top-bar cart badge.
25
25
  * Best-effort: a cart error just yields 0 (badge hidden), never fails the screen. */
@@ -74,9 +74,7 @@ function productPolicyBody(product: StoreProduct, kind: "returns" | "shipping"):
74
74
  });
75
75
  if (!section) return "";
76
76
  const html = "plainDescription" in section ? section.plainDescription : undefined;
77
- if (html) return plainText(html.replace(/&nbsp;/gi, " "));
78
- if (typeof section.description === "string") return plainText(section.description.replace(/&nbsp;/gi, " "));
79
- return richBody(section.description).bodyText;
77
+ return descriptionBody(html || section.description).bodyText;
80
78
  }
81
79
 
82
80
  /** The product id `lastProduct` was read for. A pooled screen re-appearing calls
@@ -140,13 +138,12 @@ function optionsHint(groups: StoreOption[], selection: StoreOptionSelection): st
140
138
  return copyTemplate("product.optionsChoose", "Choose {options}", { options: missing.join(", ") });
141
139
  }
142
140
 
143
- /** The description pair. V3's `description` is a Ricos OBJECT (the HTML `plainText` would throw on it): it
144
- * becomes the RichContent document, and `plainDescription` (or the document's own text) the plain copy. A V1
145
- * `description` is an HTML string: stripped text, no document. */
141
+ /** Catalog V3 returns Ricos objects or serialized documents; V1 returns HTML.
142
+ * Keep plainDescription as the independent fallback, never raw document JSON. */
146
143
  function productBody(p: StoreProduct): { description: string; descriptionJson: string } {
147
144
  const raw = (p as { description?: unknown }).description;
148
- const html = plainText(p.plainDescription || (typeof raw === "string" ? raw : ""));
149
- const { bodyJson, bodyText } = richBody(typeof raw === "string" ? null : raw, html);
145
+ const html = descriptionBody(p.plainDescription).bodyText;
146
+ const { bodyJson, bodyText } = descriptionBody(raw, html);
150
147
  return { description: html || bodyText, descriptionJson: bodyJson };
151
148
  }
152
149
 
@@ -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","local_database","clipboard","ui","speech","camera","photos","file_picker","files","presigned_upload","app_info","biometrics","wix_platform","animation"],
40
+ capabilities: ["live_activity","push","share","secure_storage","open_url","local_database","clipboard","ui","speech","camera","photos","file_picker","files","presigned_upload","app_info","biometrics","wix_platform","stream","wix_api"],
41
41
  designTokensPath: "./tokens.json",
42
42
  // Native fallback; the T5 splash descriptor owns the editable launch appearance.
43
43
  splash: {
@@ -52741,8 +52741,7 @@ const SCREENS = runtimeSetting({
52741
52741
  "params": {
52742
52742
  "screenId": "himi_owner_lite_search",
52743
52743
  "title": "Search records",
52744
- "subtitle": "Across your business areas",
52745
- "search": "drawer"
52744
+ "subtitle": "Across your business areas"
52746
52745
  }
52747
52746
  },
52748
52747
  "pressFeedback": "scale",
@@ -54261,7 +54260,7 @@ const SCREENS = runtimeSetting({
54261
54260
  "actionId": "tier5.himiOwnerLiteSearch.search"
54262
54261
  },
54263
54262
  "debounceMs": 200,
54264
- "placement": "drawer"
54263
+ "loadingPath": "$service:himiOwnerLiteSearch.anyLoading"
54265
54264
  }
54266
54265
  }
54267
54266
  },
@@ -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
  {
@@ -23573,6 +23573,9 @@ const SCREENS = runtimeSetting({
23573
23573
  "style": "body",
23574
23574
  "color": "textSecondary"
23575
23575
  }
23576
+ },
23577
+ "modifiers": {
23578
+ "hiddenPath": "$service:himiMyEvents.signInRequired"
23576
23579
  }
23577
23580
  },
23578
23581
  {
@@ -23587,6 +23590,32 @@ const SCREENS = runtimeSetting({
23587
23590
  "actionId": "tier5.himiMyEvents.load"
23588
23591
  }
23589
23592
  }
23593
+ },
23594
+ "modifiers": {
23595
+ "hiddenPath": "$service:himiMyEvents.signInRequired"
23596
+ }
23597
+ },
23598
+ {
23599
+ "id": "himi_my_events.retry.sign_in",
23600
+ "body": {
23601
+ "package": "himalaya",
23602
+ "type": "Button",
23603
+ "props": {
23604
+ "title": "Sign in",
23605
+ "style": "primary",
23606
+ "action": {
23607
+ "actionId": "himi.open",
23608
+ "params": {
23609
+ "screenId": "himi_sign_in",
23610
+ "mode": "signin",
23611
+ "modal": "1"
23612
+ },
23613
+ "presentation": "sheet"
23614
+ }
23615
+ }
23616
+ },
23617
+ "modifiers": {
23618
+ "hiddenPath": "$service:himiMyEvents.noSignIn"
23590
23619
  }
23591
23620
  }
23592
23621
  ]
@@ -24017,7 +24046,11 @@ const SCREENS = runtimeSetting({
24017
24046
  "bottom": 24
24018
24047
  },
24019
24048
  "onAppear": {
24020
- "actionId": "tier5.himiMyEvents.load"
24049
+ "actionId": "tier5.himiMyEvents.load",
24050
+ "params": {
24051
+ "signInScreenId": "himi_sign_in",
24052
+ "memberOnly": "1"
24053
+ }
24021
24054
  }
24022
24055
  }
24023
24056
  },
@@ -32759,6 +32792,31 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
32759
32792
  "hiddenPath": "$service:himiBrandedProgram.hidePriceRetry"
32760
32793
  }
32761
32794
  },
32795
+ {
32796
+ "id": "himi_program.description_rich",
32797
+ "body": {
32798
+ "package": "himalaya",
32799
+ "type": "RichContent",
32800
+ "props": {
32801
+ "documentJsonPath": "$service:himiBrandedProgram.descriptionJson",
32802
+ "onLink": {
32803
+ "actionId": "tier5.himiBrandedProgram.openLink"
32804
+ },
32805
+ "fallback": {
32806
+ "id": "",
32807
+ "body": {
32808
+ "package": "himalaya",
32809
+ "type": "Text",
32810
+ "props": {
32811
+ "text": "$service:himiBrandedProgram.description",
32812
+ "style": "body",
32813
+ "color": "textSecondary"
32814
+ }
32815
+ }
32816
+ }
32817
+ }
32818
+ }
32819
+ },
32762
32820
  {
32763
32821
  "id": "",
32764
32822
  "body": {
@@ -32770,6 +32828,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
32770
32828
  "color": "textSecondary",
32771
32829
  "collapseWhenEmpty": true
32772
32830
  }
32831
+ },
32832
+ "modifiers": {
32833
+ "hiddenPath": "$service:himiBrandedProgram.hasDescriptionJson"
32773
32834
  }
32774
32835
  }
32775
32836
  ]
@@ -36565,6 +36626,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
36565
36626
  "style": "body",
36566
36627
  "color": "textSecondary"
36567
36628
  }
36629
+ },
36630
+ "modifiers": {
36631
+ "hiddenPath": "$service:himiBlogPost.signInRequired"
36568
36632
  }
36569
36633
  },
36570
36634
  {
@@ -36579,6 +36643,32 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
36579
36643
  "actionId": "tier5.himiBlogPost.retry"
36580
36644
  }
36581
36645
  }
36646
+ },
36647
+ "modifiers": {
36648
+ "hiddenPath": "$service:himiBlogPost.signInRequired"
36649
+ }
36650
+ },
36651
+ {
36652
+ "id": "himi_blog_post.retry.sign_in",
36653
+ "body": {
36654
+ "package": "himalaya",
36655
+ "type": "Button",
36656
+ "props": {
36657
+ "title": "Sign in",
36658
+ "style": "primary",
36659
+ "action": {
36660
+ "actionId": "himi.open",
36661
+ "params": {
36662
+ "screenId": "himi_sign_in",
36663
+ "mode": "signin",
36664
+ "modal": "1"
36665
+ },
36666
+ "presentation": "sheet"
36667
+ }
36668
+ }
36669
+ },
36670
+ "modifiers": {
36671
+ "hiddenPath": "$service:himiBlogPost.noSignIn"
36582
36672
  }
36583
36673
  }
36584
36674
  ]
@@ -37283,7 +37373,8 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
37283
37373
  "onAppear": {
37284
37374
  "actionId": "tier5.himiBlogPost.load",
37285
37375
  "params": {
37286
- "id": "__HIMI_ID__"
37376
+ "id": "__HIMI_ID__",
37377
+ "signInScreenId": "himi_sign_in"
37287
37378
  }
37288
37379
  }
37289
37380
  }
@@ -0,0 +1,52 @@
1
+ import { memberAccess, memberAccessChanged, memberAccessDenied, type MemberAccess } from "./member-access.js";
2
+ import { needsSignIn } from "./branded-load-state.js";
3
+
4
+ /** Public reads stay public. Capture their reader only to reject a result that
5
+ * belongs to the identity/site which was active before the request. */
6
+ export interface ReadAccess { member: MemberAccess; tenant: string; }
7
+ export async function readAccess(): Promise<ReadAccess> {
8
+ const member = await memberAccess();
9
+ const input = (globalThis as { himalaya?: { input?: Record<string, unknown> } }).himalaya?.input;
10
+ return { member, tenant: JSON.stringify([input?.__himiPreferenceAccount ?? "", input?.__himiPreferenceBusiness ?? ""]) };
11
+ }
12
+
13
+ export async function assertReadAccess(access: ReadAccess): Promise<void> {
14
+ const current = await readAccess();
15
+ if (current.member.email !== access.member.email || current.member.scope !== access.member.scope || current.tenant !== access.tenant) throw memberAccessChanged();
16
+ }
17
+
18
+ /** Bridge errors put the actual status first; codes quoted inside the body are data. */
19
+ function httpStatus(error: unknown): string | undefined {
20
+ return /^HTTP\s*(\d{3})\b/i.exec(String((error as { message?: unknown })?.message ?? error))?.[1];
21
+ }
22
+
23
+ export const NO_READ_SIGN_IN = { signInRequired: false, noSignIn: true };
24
+
25
+ /** An authorization refusal must not fall back to a previously cached article. */
26
+ export function readAccessDenied(error: unknown): boolean {
27
+ let cause = error;
28
+ for (let depth = 0; cause && depth < 4; depth++) {
29
+ const status = httpStatus(cause);
30
+ if (status ? status === "401" || status === "403" : memberAccessDenied(cause) || needsSignIn(cause)) return true;
31
+ cause = (cause as { cause?: unknown }).cause;
32
+ }
33
+ return false;
34
+ }
35
+
36
+ /** A permission failure alone says nothing about whether signing in can help.
37
+ * In particular, a private member profile can be unavailable to members too.
38
+ * Only an authentication refusal for a current visitor offers Sign in. Follow
39
+ * SDK wrapper causes (blog getPost preserves its original read error there). */
40
+ export async function readSignInFlags(error: unknown): Promise<typeof NO_READ_SIGN_IN> {
41
+ let authenticationRequired = false;
42
+ let permissionRefused = false;
43
+ let cause = error;
44
+ for (let depth = 0; cause && depth < 4; depth++) {
45
+ const status = httpStatus(cause);
46
+ permissionRefused ||= status === "403";
47
+ authenticationRequired ||= status ? status === "401" : needsSignIn(cause);
48
+ cause = (cause as { cause?: unknown }).cause;
49
+ }
50
+ const signInRequired = !permissionRefused && authenticationRequired && !(await memberAccess()).email;
51
+ return { signInRequired, noSignIn: !signInRequired };
52
+ }
@@ -0,0 +1,11 @@
1
+ import type { WixProgram } from "@himalaya/wix-online-programs";
2
+
3
+ /** v3 nests the body in description.details; older responses use settings or a
4
+ * flat description. The body itself may be a Ricos document or serialized text. */
5
+ export function programDescription(program: WixProgram): unknown {
6
+ const raw = (program as { description?: unknown }).description;
7
+ const nested = raw && typeof raw === "object" ? raw as { details?: unknown; nodes?: unknown } : undefined;
8
+ return program.settings?.description ?? nested?.details
9
+ ?? (typeof raw === "string" || Array.isArray(nested?.nodes) ? raw : undefined)
10
+ ?? program.details?.description;
11
+ }