@wix/himalaya-cli 0.1570.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 (126) 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/auth.mjs +17 -7
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core/validation.mjs +17 -7
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +17 -7
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +17 -7
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/state.mjs +17 -7
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-auth.mjs +7 -1
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-blog.mjs +1 -0
  11. package/dist/cli.mjs +1672 -1206
  12. package/dist/preview-assets.generated.bin +0 -0
  13. package/guides/base44-app-to-himi-app.md +1 -1
  14. package/guides/capabilities.md +7 -0
  15. package/guides/himi-json-output.md +11 -0
  16. package/guides/one-app-to-himi-app.md +122 -4
  17. package/guides/sdk-reference.json +2 -2
  18. package/guides/splash-screens.md +10 -1
  19. package/guides/trim-a-template.md +5 -0
  20. package/guides/wix-business-context.md +11 -2
  21. package/package.json +2 -2
  22. package/templates/beauty-salon/config.ts +1 -1
  23. package/templates/beauty-salon/dev/index.ts +31 -1
  24. package/templates/beauty-salon/tier5-src/_shared/member-read.ts +52 -0
  25. package/templates/beauty-salon/tier5-src/_shared/program-description.ts +11 -0
  26. package/templates/beauty-salon/tier5-src/himi-blog-post/index.ts +35 -12
  27. package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +3 -3
  28. package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +6 -9
  29. package/templates/beauty-salon-owner/config.ts +1 -1
  30. package/templates/branded-lite/config.ts +1 -1
  31. package/templates/branded-lite/dev/index.ts +163 -2
  32. package/templates/branded-lite/tier5-src/_shared/member-read.ts +52 -0
  33. package/templates/branded-lite/tier5-src/_shared/program-description.ts +11 -0
  34. package/templates/branded-lite/tier5-src/himi-blog-post/index.ts +35 -12
  35. package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +3 -3
  36. package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +16 -14
  37. package/templates/branded-lite/tier5-src/himi-branded-lite-programs/index.ts +3 -7
  38. package/templates/branded-lite/tier5-src/himi-events-my-events/index.ts +76 -42
  39. package/templates/branded-lite/tier5-src/himi-store-product/index.ts +6 -9
  40. package/templates/branded-lite/tier5-src/wix-spaces-chat-thread/index.ts +101 -34
  41. package/templates/branded-lite/tier5-src/wix-spaces-forum-post/index.ts +23 -6
  42. package/templates/branded-lite/tier5-src/wix-spaces-member-detail/index.ts +26 -11
  43. package/templates/community-club/config.ts +1 -1
  44. package/templates/community-club/dev/index.ts +65 -2
  45. package/templates/community-club/tier5-src/_shared/branded-load-state.ts +106 -0
  46. package/templates/community-club/tier5-src/_shared/member-read.ts +52 -0
  47. package/templates/community-club/tier5-src/_shared/program-description.ts +11 -0
  48. package/templates/community-club/tier5-src/himi-blog-post/index.ts +35 -12
  49. package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +3 -3
  50. package/templates/community-club/tier5-src/himi-events-my-events/index.ts +76 -42
  51. package/templates/community-club-owner/config.ts +1 -1
  52. package/templates/events-meetups/config.ts +1 -1
  53. package/templates/events-meetups/dev/index.ts +65 -2
  54. package/templates/events-meetups/tier5-src/_shared/branded-load-state.ts +106 -0
  55. package/templates/events-meetups/tier5-src/_shared/member-read.ts +52 -0
  56. package/templates/events-meetups/tier5-src/_shared/program-description.ts +11 -0
  57. package/templates/events-meetups/tier5-src/himi-blog-post/index.ts +35 -12
  58. package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +3 -3
  59. package/templates/events-meetups/tier5-src/himi-events-my-events/index.ts +76 -42
  60. package/templates/events-meetups-owner/config.ts +1 -1
  61. package/templates/gym-fitness/config.ts +1 -1
  62. package/templates/gym-fitness/dev/index.ts +65 -2
  63. package/templates/gym-fitness/tier5-src/_shared/member-read.ts +52 -0
  64. package/templates/gym-fitness/tier5-src/_shared/program-description.ts +11 -0
  65. package/templates/gym-fitness/tier5-src/himi-blog-post/index.ts +35 -12
  66. package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +3 -3
  67. package/templates/gym-fitness/tier5-src/himi-events-my-events/index.ts +76 -42
  68. package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +6 -9
  69. package/templates/gym-fitness-owner/config.ts +1 -1
  70. package/templates/nonprofit-cause/config.ts +1 -1
  71. package/templates/nonprofit-cause/dev/index.ts +65 -2
  72. package/templates/nonprofit-cause/tier5-src/_shared/branded-load-state.ts +106 -0
  73. package/templates/nonprofit-cause/tier5-src/_shared/member-read.ts +52 -0
  74. package/templates/nonprofit-cause/tier5-src/_shared/program-description.ts +11 -0
  75. package/templates/nonprofit-cause/tier5-src/himi-blog-post/index.ts +35 -12
  76. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +3 -3
  77. package/templates/nonprofit-cause/tier5-src/himi-events-my-events/index.ts +76 -42
  78. package/templates/nonprofit-cause-owner/config.ts +1 -1
  79. package/templates/owner-lite/config.ts +1 -1
  80. package/templates/podcast-creator/config.ts +1 -1
  81. package/templates/podcast-creator/dev/index.ts +65 -2
  82. package/templates/podcast-creator/tier5-src/_shared/branded-load-state.ts +106 -0
  83. package/templates/podcast-creator/tier5-src/_shared/member-read.ts +52 -0
  84. package/templates/podcast-creator/tier5-src/_shared/program-description.ts +11 -0
  85. package/templates/podcast-creator/tier5-src/himi-blog-post/index.ts +35 -12
  86. package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +3 -3
  87. package/templates/podcast-creator/tier5-src/himi-events-my-events/index.ts +76 -42
  88. package/templates/podcast-creator-owner/config.ts +1 -1
  89. package/templates/portfolio-creative/config.ts +1 -1
  90. package/templates/portfolio-creative/dev/index.ts +65 -2
  91. package/templates/portfolio-creative/tier5-src/_shared/branded-load-state.ts +106 -0
  92. package/templates/portfolio-creative/tier5-src/_shared/member-read.ts +52 -0
  93. package/templates/portfolio-creative/tier5-src/_shared/program-description.ts +11 -0
  94. package/templates/portfolio-creative/tier5-src/himi-blog-post/index.ts +35 -12
  95. package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +3 -3
  96. package/templates/portfolio-creative/tier5-src/himi-events-my-events/index.ts +76 -42
  97. package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +6 -9
  98. package/templates/portfolio-creative-owner/config.ts +1 -1
  99. package/templates/restaurant/config.ts +1 -1
  100. package/templates/restaurant/dev/index.ts +65 -2
  101. package/templates/restaurant/tier5-src/_shared/branded-load-state.ts +106 -0
  102. package/templates/restaurant/tier5-src/_shared/member-read.ts +52 -0
  103. package/templates/restaurant/tier5-src/_shared/program-description.ts +11 -0
  104. package/templates/restaurant/tier5-src/himi-blog-post/index.ts +35 -12
  105. package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +3 -3
  106. package/templates/restaurant/tier5-src/himi-events-my-events/index.ts +76 -42
  107. package/templates/restaurant-owner/config.ts +1 -1
  108. package/templates/retail-store/config.ts +1 -1
  109. package/templates/retail-store/dev/index.ts +31 -1
  110. package/templates/retail-store/tier5-src/_shared/branded-load-state.ts +106 -0
  111. package/templates/retail-store/tier5-src/_shared/member-read.ts +52 -0
  112. package/templates/retail-store/tier5-src/_shared/program-description.ts +11 -0
  113. package/templates/retail-store/tier5-src/himi-blog-post/index.ts +35 -12
  114. package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +3 -3
  115. package/templates/retail-store/tier5-src/himi-store-product/index.ts +6 -9
  116. package/templates/retail-store-owner/config.ts +1 -1
  117. package/templates/yoga-studio/config.ts +1 -1
  118. package/templates/yoga-studio/dev/index.ts +93 -2
  119. package/templates/yoga-studio/tier5-src/_shared/member-read.ts +52 -0
  120. package/templates/yoga-studio/tier5-src/_shared/program-description.ts +11 -0
  121. package/templates/yoga-studio/tier5-src/himi-blog-post/index.ts +35 -12
  122. package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +3 -3
  123. package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +16 -14
  124. package/templates/yoga-studio/tier5-src/himi-branded-lite-programs/index.ts +3 -7
  125. package/templates/yoga-studio/tier5-src/himi-events-my-events/index.ts +76 -42
  126. package/templates/yoga-studio-owner/config.ts +1 -1
@@ -32197,6 +32197,9 @@ const SCREENS = runtimeSetting({
32197
32197
  "style": "body",
32198
32198
  "color": "textSecondary"
32199
32199
  }
32200
+ },
32201
+ "modifiers": {
32202
+ "hiddenPath": "$service:himiMyEvents.signInRequired"
32200
32203
  }
32201
32204
  },
32202
32205
  {
@@ -32211,6 +32214,32 @@ const SCREENS = runtimeSetting({
32211
32214
  "actionId": "tier5.himiMyEvents.load"
32212
32215
  }
32213
32216
  }
32217
+ },
32218
+ "modifiers": {
32219
+ "hiddenPath": "$service:himiMyEvents.signInRequired"
32220
+ }
32221
+ },
32222
+ {
32223
+ "id": "himi_my_events.retry.sign_in",
32224
+ "body": {
32225
+ "package": "himalaya",
32226
+ "type": "Button",
32227
+ "props": {
32228
+ "title": "Sign in",
32229
+ "style": "primary",
32230
+ "action": {
32231
+ "actionId": "himi.open",
32232
+ "params": {
32233
+ "screenId": "himi_sign_in",
32234
+ "mode": "signin",
32235
+ "modal": "1"
32236
+ },
32237
+ "presentation": "sheet"
32238
+ }
32239
+ }
32240
+ },
32241
+ "modifiers": {
32242
+ "hiddenPath": "$service:himiMyEvents.noSignIn"
32214
32243
  }
32215
32244
  }
32216
32245
  ]
@@ -32641,7 +32670,11 @@ const SCREENS = runtimeSetting({
32641
32670
  "bottom": 24
32642
32671
  },
32643
32672
  "onAppear": {
32644
- "actionId": "tier5.himiMyEvents.load"
32673
+ "actionId": "tier5.himiMyEvents.load",
32674
+ "params": {
32675
+ "signInScreenId": "himi_sign_in",
32676
+ "memberOnly": "1"
32677
+ }
32645
32678
  }
32646
32679
  }
32647
32680
  },
@@ -48268,6 +48301,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
48268
48301
  "style": "body",
48269
48302
  "color": "textSecondary"
48270
48303
  }
48304
+ },
48305
+ "modifiers": {
48306
+ "hiddenPath": "$service:himiBlogPost.signInRequired"
48271
48307
  }
48272
48308
  },
48273
48309
  {
@@ -48282,6 +48318,32 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
48282
48318
  "actionId": "tier5.himiBlogPost.retry"
48283
48319
  }
48284
48320
  }
48321
+ },
48322
+ "modifiers": {
48323
+ "hiddenPath": "$service:himiBlogPost.signInRequired"
48324
+ }
48325
+ },
48326
+ {
48327
+ "id": "himi_blog_post.retry.sign_in",
48328
+ "body": {
48329
+ "package": "himalaya",
48330
+ "type": "Button",
48331
+ "props": {
48332
+ "title": "Sign in",
48333
+ "style": "primary",
48334
+ "action": {
48335
+ "actionId": "himi.open",
48336
+ "params": {
48337
+ "screenId": "himi_sign_in",
48338
+ "mode": "signin",
48339
+ "modal": "1"
48340
+ },
48341
+ "presentation": "sheet"
48342
+ }
48343
+ }
48344
+ },
48345
+ "modifiers": {
48346
+ "hiddenPath": "$service:himiBlogPost.noSignIn"
48285
48347
  }
48286
48348
  }
48287
48349
  ]
@@ -48986,7 +49048,8 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
48986
49048
  "onAppear": {
48987
49049
  "actionId": "tier5.himiBlogPost.load",
48988
49050
  "params": {
48989
- "id": "__HIMI_ID__"
49051
+ "id": "__HIMI_ID__",
49052
+ "signInScreenId": "himi_sign_in"
48990
49053
  }
48991
49054
  }
48992
49055
  }
@@ -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,
@@ -5,6 +5,8 @@ import { queryEvents, type WixEvent } from "@himalaya/wix-events";
5
5
  import { getAll, upsert, type DbEntity } from "@himalaya/local-db";
6
6
  import { confirmedRsvpIds, eventStartMillis, normalizeEvent, type EventRow } from "../_shared/himi-events";
7
7
  import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
8
+ import { memberAccess } from "../_shared/member-access.js";
9
+ import { readAccess, assertReadAccess, readAccessDenied, readSignInFlags, NO_READ_SIGN_IN } from "../_shared/member-read.js";
8
10
 
9
11
  // Offline-first (data-offline): My Events is a read-through over the same `events`
10
12
  // catalog the list caches — paint the last-synced rows (filtered to this device's
@@ -27,6 +29,7 @@ async function writeCached(rows: CachedEventRow[]): Promise<void> {
27
29
  }
28
30
 
29
31
  interface S {
32
+ signInRequired: boolean; noSignIn: boolean; signInScreenId: string;
30
33
  status: "loading" | "ready";
31
34
  events: EventRow[];
32
35
  emptyText: string;
@@ -98,11 +101,10 @@ function unverifiedEvent(event: CachedEventRow): CachedEventRow {
98
101
  };
99
102
  }
100
103
 
101
- /** Keep only the candidate rows the member ACTUALLY still holds an RSVP for, asking
102
- * Wix per event. Fail-open per row: a network/permission blip must not blank a row the
103
- * member really has (that would look like their RSVP vanished), so an errored probe
104
- * keeps the row and only an explicit "no active RSVP" drops it. */
105
- async function confirmRsvps(candidates: CachedEventRow[]): Promise<{ events: CachedEventRow[]; verificationUnavailable: boolean }> {
104
+ /** Verify device-local candidates against the current member. Member-only mode
105
+ * requires successful verification; standalone mode retains failed probes as
106
+ * explicitly unverified device-local history. */
107
+ async function confirmRsvps(candidates: CachedEventRow[], memberOnly = false): Promise<{ events: CachedEventRow[]; verificationUnavailable: boolean }> {
106
108
  // Raw wixApi chokepoint rather than a generated client: the member RSVP read lives in
107
109
  // events **v2**, and `@himalaya/wix-events` is the v1 domain module. Adding a
108
110
  // `wix-events-v2` specifier is not an option — the umbrella package deliberately
@@ -122,7 +124,9 @@ async function confirmRsvps(candidates: CachedEventRow[]): Promise<{ events: Cac
122
124
  event: held ? { ...event, status: [event.status, copyParam("myEvents.rsvpConfirmed", "RSVP confirmed")].filter(Boolean).join(" · ") } : null,
123
125
  verificationUnavailable: false,
124
126
  };
125
- } catch {
127
+ } catch (error) {
128
+ // Device candidates carry no member ownership proof until this read succeeds.
129
+ if (memberOnly) throw error;
126
130
  // Fail-open, but never claim backend confirmation while its member-readable
127
131
  // probe is unavailable. The device id remains a candidate, explicitly labeled.
128
132
  return { event: unverifiedEvent(event), verificationUnavailable: true };
@@ -134,8 +138,8 @@ async function confirmRsvps(candidates: CachedEventRow[]): Promise<{ events: Cac
134
138
  };
135
139
  }
136
140
 
137
- const svc = defineService<S, { load: (s: S, arg?: unknown) => Promise<S> }>("himiMyEvents", {
138
- state: derive({
141
+ const INITIAL: S = derive({
142
+ ...NO_READ_SIGN_IN, signInScreenId: "himi_account",
139
143
  status: "loading",
140
144
  events: [],
141
145
  emptyText: "",
@@ -147,45 +151,75 @@ const svc = defineService<S, { load: (s: S, arg?: unknown) => Promise<S> }>("him
147
151
  isLoading: true,
148
152
  notLoading: false,
149
153
  noEmpty: true,
150
- }),
154
+ });
155
+ let memberOnlyRequested = false;
156
+ let loadVersion = 0;
157
+ const svc = defineService<S, { prepare: (s: S) => S; load: (s: S, arg?: unknown) => Promise<S | undefined> }>("himiMyEvents", {
158
+ state: INITIAL,
151
159
  actions: {
152
- load: async (s, arg) => {
160
+ prepare: (s) => ({ ...s, ...INITIAL }),
161
+ load: async (s, arg): Promise<S | undefined> => {
153
162
  stashCopy(arg);
154
- const confirmed = new Set(confirmedRsvpIds());
155
- const identity = await identityLine();
156
- // 1) Instant paint from the on-device cache (offline-safe; empty on first-ever run).
157
- const cached = await readCached();
158
- const cachedEvents = cached.filter((event) => confirmed.has(event.id)).map(displayEventRow);
159
- const base = cached.length
160
- ? derive({ ...s, status: "ready", errored: false, events: cachedEvents, identityLine: identity, verificationUnavailable: cachedEvents.length > 0 })
161
- : s;
162
- // 2) Revalidate from Wix and refresh the cache.
163
+ const version = ++loadVersion;
164
+ const options = arg as { memberOnly?: unknown; signInScreenId?: unknown } | undefined;
165
+ if (options?.memberOnly !== undefined) memberOnlyRequested = options.memberOnly === "1";
166
+ const memberOnly = memberOnlyRequested;
167
+ const clean = { ...INITIAL, signInScreenId: options?.signInScreenId === "himi_sign_in" ? "himi_sign_in" : s.signInScreenId };
168
+ await svc.prepare();
163
169
  try {
164
- const res = await queryEvents({ limit: 50 });
165
- const all = ((res as unknown as { events?: WixEvent[] }).events ?? []).map(cachedEventRow);
166
- await writeCached(all);
167
- // Phase 3c — make each row AUTHORITATIVE. The device list is only a candidate
168
- // set: it never forgets, so an RSVP cancelled elsewhere (web, or by the
169
- // business) would otherwise show here forever. Confirm each candidate against
170
- // Wix and drop the ones the member no longer holds.
171
- //
172
- // Per-event by necessity, not by choice: a member CANNOT enumerate their own
173
- // RSVPs. Verified live against a real member token — `queryRsvps` (cross-event)
174
- // 403s "WIX_EVENTS.READ_RSVP permission required", `listOrders` 403s
175
- // READ_ORDERS, and `/rsvps/current` 400s without an eventId. Only the
176
- // single-event read is member-readable, so the fan-out below IS the ceiling
177
- // until those member scopes exist server-side.
178
- const candidates = all.filter((event) => confirmed.has(event.id)).map(displayEventRow);
179
- const confirmation = await confirmRsvps(candidates);
180
- return derive({ ...base, status: "ready", errored: false, events: confirmation.events, identityLine: identity, verificationUnavailable: confirmation.verificationUnavailable });
181
- } catch {
182
- // Offline / fetch failure: keep the cached rows instead of a blank error state.
183
- return cached.length
184
- ? base
185
- : derive({ ...s, status: "ready", errored: true, events: [], identityLine: identity });
170
+ const member = memberOnly ? await readAccess() : null;
171
+ if (memberOnly && !member?.member.email) throw new Error("Unauthenticated: sign in to see your events");
172
+ const confirmed = new Set(confirmedRsvpIds());
173
+ const identity = await identityLine();
174
+ // 1) Instant paint from the on-device cache (offline-safe; empty on first-ever run).
175
+ // Device-local candidates have no member ownership proof. Branded must
176
+ // verify them online rather than show a previous member's cached RSVP list.
177
+ const cached = memberOnly ? [] : await readCached();
178
+ const cachedEvents = cached.filter((event) => confirmed.has(event.id)).map(displayEventRow);
179
+ const base = cached.length
180
+ ? derive({ ...clean, status: "ready", errored: false, events: cachedEvents, identityLine: identity, verificationUnavailable: cachedEvents.length > 0 })
181
+ : clean;
182
+ // 2) Revalidate from Wix and refresh the cache.
183
+ try {
184
+ const res = await queryEvents({ limit: 50 });
185
+ const all = ((res as unknown as { events?: WixEvent[] }).events ?? []).map(cachedEventRow);
186
+ if (member) await assertReadAccess(member);
187
+ if (version !== loadVersion) return undefined;
188
+ await writeCached(all);
189
+ // Phase 3c — make each row AUTHORITATIVE. The device list is only a candidate
190
+ // set: it never forgets, so an RSVP cancelled elsewhere (web, or by the
191
+ // business) would otherwise show here forever. Confirm each candidate against
192
+ // Wix and drop the ones the member no longer holds.
193
+ //
194
+ // Per-event by necessity, not by choice: a member CANNOT enumerate their own
195
+ // RSVPs. Verified live against a real member token — `queryRsvps` (cross-event)
196
+ // 403s "WIX_EVENTS.READ_RSVP permission required", `listOrders` 403s
197
+ // READ_ORDERS, and `/rsvps/current` 400s without an eventId. Only the
198
+ // single-event read is member-readable, so the fan-out below IS the ceiling
199
+ // until those member scopes exist server-side.
200
+ const candidates = all.filter((event) => confirmed.has(event.id)).map(displayEventRow);
201
+ const confirmation = await confirmRsvps(candidates, memberOnly);
202
+ if (member) await assertReadAccess(member);
203
+ if (version !== loadVersion) return undefined;
204
+ return derive({ ...base, status: "ready", errored: false, events: confirmation.events, identityLine: identity, verificationUnavailable: confirmation.verificationUnavailable });
205
+ } catch (error) {
206
+ if (memberOnly && readAccessDenied(error)) throw error;
207
+ if (member) await assertReadAccess(member);
208
+ if (version !== loadVersion) return undefined;
209
+ // Offline / fetch failure: keep the cached rows instead of a blank error state.
210
+ return cached.length
211
+ ? base
212
+ : derive({ ...clean, status: "ready", errored: true, events: [], identityLine: identity });
213
+ }
214
+ } catch (error) {
215
+ const failure = memberOnly && !(await memberAccess()).email ? new Error("Unauthenticated: sign in to see your events") : error;
216
+ const flags = await readSignInFlags(failure);
217
+ return version === loadVersion ? derive({ ...clean, ...flags, status: "ready", errored: true, events: [] }) : undefined;
186
218
  }
187
219
  },
188
220
  },
189
221
  });
190
- svc.load();
222
+ // Wait for the descriptor's load action to select member-only or device-local scope.
223
+ // An eager unconfigured read could briefly expose guest KV before Branded opts in.
224
+ svc.prepare();
191
225
  export { svc };
@@ -15,7 +15,7 @@ import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
15
15
  import { isBridgeUnavailable } from "../_shared/owner-load-error.js";
16
16
  import { isProductSaved, toggleSavedProduct } from "../_shared/himi-store-saved.js";
17
17
  import { openRichLink } from "../_shared/rich-content-link.js";
18
- import { richBody } from "../_shared/rich-body.js";
18
+ import { descriptionBody } from "../_shared/rich-body.js";
19
19
 
20
20
  // Offline-first (data-offline): the detail is a public READ-THROUGH — when the live
21
21
  // product fetch fails (offline) fall back to the last-synced row the catalog cached
@@ -85,9 +85,7 @@ function productPolicyBody(product: StoreProduct, kind: "returns" | "shipping"):
85
85
  });
86
86
  if (!section) return "";
87
87
  const html = "plainDescription" in section ? section.plainDescription : undefined;
88
- if (html) return plainText(html.replace(/&nbsp;/gi, " "));
89
- if (typeof section.description === "string") return plainText(section.description.replace(/&nbsp;/gi, " "));
90
- return richBody(section.description).bodyText;
88
+ return descriptionBody(html || section.description).bodyText;
91
89
  }
92
90
 
93
91
  /** The product id `lastProduct` was read for. A pooled screen re-appearing calls
@@ -170,13 +168,12 @@ function optionsHint(groups: StoreOption[], selection: StoreOptionSelection): st
170
168
  return copyTemplate("product.optionsChoose", "Choose {options}", { options: missing.join(", ") });
171
169
  }
172
170
 
173
- /** The description pair. V3's `description` is a Ricos OBJECT (the HTML `plainText` would throw on it): it
174
- * becomes the RichContent document, and `plainDescription` (or the document's own text) the plain copy. A V1
175
- * `description` is an HTML string: stripped text, no document. */
171
+ /** Catalog V3 returns Ricos objects or serialized documents; V1 returns HTML.
172
+ * Keep plainDescription as the independent fallback, never raw document JSON. */
176
173
  function productBody(p: StoreProduct): { description: string; descriptionJson: string } {
177
174
  const raw = (p as { description?: unknown }).description;
178
- const html = plainText(p.plainDescription || (typeof raw === "string" ? raw : ""));
179
- const { bodyJson, bodyText } = richBody(typeof raw === "string" ? null : raw, html);
175
+ const html = descriptionBody(p.plainDescription).bodyText;
176
+ const { bodyJson, bodyText } = descriptionBody(raw, html);
180
177
  return { description: html || bodyText, descriptionJson: bodyJson };
181
178
  }
182
179
 
@@ -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: {
@@ -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
  {
@@ -15841,6 +15841,9 @@ const SCREENS = runtimeSetting({
15841
15841
  "style": "body",
15842
15842
  "color": "textSecondary"
15843
15843
  }
15844
+ },
15845
+ "modifiers": {
15846
+ "hiddenPath": "$service:himiMyEvents.signInRequired"
15844
15847
  }
15845
15848
  },
15846
15849
  {
@@ -15855,6 +15858,32 @@ const SCREENS = runtimeSetting({
15855
15858
  "actionId": "tier5.himiMyEvents.load"
15856
15859
  }
15857
15860
  }
15861
+ },
15862
+ "modifiers": {
15863
+ "hiddenPath": "$service:himiMyEvents.signInRequired"
15864
+ }
15865
+ },
15866
+ {
15867
+ "id": "himi_my_events.retry.sign_in",
15868
+ "body": {
15869
+ "package": "himalaya",
15870
+ "type": "Button",
15871
+ "props": {
15872
+ "title": "Sign in",
15873
+ "style": "primary",
15874
+ "action": {
15875
+ "actionId": "himi.open",
15876
+ "params": {
15877
+ "screenId": "himi_sign_in",
15878
+ "mode": "signin",
15879
+ "modal": "1"
15880
+ },
15881
+ "presentation": "sheet"
15882
+ }
15883
+ }
15884
+ },
15885
+ "modifiers": {
15886
+ "hiddenPath": "$service:himiMyEvents.noSignIn"
15858
15887
  }
15859
15888
  }
15860
15889
  ]
@@ -16285,7 +16314,11 @@ const SCREENS = runtimeSetting({
16285
16314
  "bottom": 24
16286
16315
  },
16287
16316
  "onAppear": {
16288
- "actionId": "tier5.himiMyEvents.load"
16317
+ "actionId": "tier5.himiMyEvents.load",
16318
+ "params": {
16319
+ "signInScreenId": "himi_sign_in",
16320
+ "memberOnly": "1"
16321
+ }
16289
16322
  }
16290
16323
  }
16291
16324
  },
@@ -25492,6 +25525,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
25492
25525
  "style": "body",
25493
25526
  "color": "textSecondary"
25494
25527
  }
25528
+ },
25529
+ "modifiers": {
25530
+ "hiddenPath": "$service:himiBlogPost.signInRequired"
25495
25531
  }
25496
25532
  },
25497
25533
  {
@@ -25506,6 +25542,32 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
25506
25542
  "actionId": "tier5.himiBlogPost.retry"
25507
25543
  }
25508
25544
  }
25545
+ },
25546
+ "modifiers": {
25547
+ "hiddenPath": "$service:himiBlogPost.signInRequired"
25548
+ }
25549
+ },
25550
+ {
25551
+ "id": "himi_blog_post.retry.sign_in",
25552
+ "body": {
25553
+ "package": "himalaya",
25554
+ "type": "Button",
25555
+ "props": {
25556
+ "title": "Sign in",
25557
+ "style": "primary",
25558
+ "action": {
25559
+ "actionId": "himi.open",
25560
+ "params": {
25561
+ "screenId": "himi_sign_in",
25562
+ "mode": "signin",
25563
+ "modal": "1"
25564
+ },
25565
+ "presentation": "sheet"
25566
+ }
25567
+ }
25568
+ },
25569
+ "modifiers": {
25570
+ "hiddenPath": "$service:himiBlogPost.noSignIn"
25509
25571
  }
25510
25572
  }
25511
25573
  ]
@@ -26210,7 +26272,8 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
26210
26272
  "onAppear": {
26211
26273
  "actionId": "tier5.himiBlogPost.load",
26212
26274
  "params": {
26213
- "id": "__HIMI_ID__"
26275
+ "id": "__HIMI_ID__",
26276
+ "signInScreenId": "himi_sign_in"
26214
26277
  }
26215
26278
  }
26216
26279
  }