@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
@@ -37014,6 +37014,29 @@ const SCREENS = runtimeSetting({
37014
37014
  ]
37015
37015
  }
37016
37016
  }
37017
+ },
37018
+ {
37019
+ "id": "himi_member.sign_in",
37020
+ "body": {
37021
+ "package": "himalaya",
37022
+ "type": "Button",
37023
+ "props": {
37024
+ "title": "Sign in",
37025
+ "style": "primary",
37026
+ "action": {
37027
+ "actionId": "himi.open",
37028
+ "params": {
37029
+ "screenId": "himi_sign_in",
37030
+ "mode": "signin",
37031
+ "modal": "1"
37032
+ },
37033
+ "presentation": "sheet"
37034
+ }
37035
+ }
37036
+ },
37037
+ "modifiers": {
37038
+ "hiddenPath": "$service:spacesMemberDetail.noSignIn"
37039
+ }
37017
37040
  }
37018
37041
  ]
37019
37042
  }
@@ -37531,6 +37554,7 @@ const SCREENS = runtimeSetting({
37531
37554
  }
37532
37555
  },
37533
37556
  "modifiers": {
37557
+ "hiddenPath": "$service:spacesChatThread.signInRequired",
37534
37558
  "padding": {
37535
37559
  "leading": 16,
37536
37560
  "trailing": 16,
@@ -37538,6 +37562,29 @@ const SCREENS = runtimeSetting({
37538
37562
  "bottom": 0
37539
37563
  }
37540
37564
  }
37565
+ },
37566
+ {
37567
+ "id": "himi_chat_thread.sign_in",
37568
+ "body": {
37569
+ "package": "himalaya",
37570
+ "type": "Button",
37571
+ "props": {
37572
+ "title": "Sign in",
37573
+ "style": "primary",
37574
+ "action": {
37575
+ "actionId": "himi.open",
37576
+ "params": {
37577
+ "screenId": "himi_sign_in",
37578
+ "mode": "signin",
37579
+ "modal": "1"
37580
+ },
37581
+ "presentation": "sheet"
37582
+ }
37583
+ }
37584
+ },
37585
+ "modifiers": {
37586
+ "hiddenPath": "$service:spacesChatThread.noSignIn"
37587
+ }
37541
37588
  }
37542
37589
  ]
37543
37590
  }
@@ -57216,6 +57263,29 @@ const SCREENS = runtimeSetting({
57216
57263
  ]
57217
57264
  }
57218
57265
  }
57266
+ },
57267
+ {
57268
+ "id": "himi_forum_post.sign_in",
57269
+ "body": {
57270
+ "package": "himalaya",
57271
+ "type": "Button",
57272
+ "props": {
57273
+ "title": "Sign in",
57274
+ "style": "primary",
57275
+ "action": {
57276
+ "actionId": "himi.open",
57277
+ "params": {
57278
+ "screenId": "himi_sign_in",
57279
+ "mode": "signin",
57280
+ "modal": "1"
57281
+ },
57282
+ "presentation": "sheet"
57283
+ }
57284
+ }
57285
+ },
57286
+ "modifiers": {
57287
+ "hiddenPath": "$service:spacesForumPost.noSignIn"
57288
+ }
57219
57289
  }
57220
57290
  ]
57221
57291
  }
@@ -60393,6 +60463,9 @@ const SCREENS = runtimeSetting({
60393
60463
  "style": "body",
60394
60464
  "color": "textSecondary"
60395
60465
  }
60466
+ },
60467
+ "modifiers": {
60468
+ "hiddenPath": "$service:himiMyEvents.signInRequired"
60396
60469
  }
60397
60470
  },
60398
60471
  {
@@ -60407,6 +60480,32 @@ const SCREENS = runtimeSetting({
60407
60480
  "actionId": "tier5.himiMyEvents.load"
60408
60481
  }
60409
60482
  }
60483
+ },
60484
+ "modifiers": {
60485
+ "hiddenPath": "$service:himiMyEvents.signInRequired"
60486
+ }
60487
+ },
60488
+ {
60489
+ "id": "himi_my_events.retry.sign_in",
60490
+ "body": {
60491
+ "package": "himalaya",
60492
+ "type": "Button",
60493
+ "props": {
60494
+ "title": "Sign in",
60495
+ "style": "primary",
60496
+ "action": {
60497
+ "actionId": "himi.open",
60498
+ "params": {
60499
+ "screenId": "himi_sign_in",
60500
+ "mode": "signin",
60501
+ "modal": "1"
60502
+ },
60503
+ "presentation": "sheet"
60504
+ }
60505
+ }
60506
+ },
60507
+ "modifiers": {
60508
+ "hiddenPath": "$service:himiMyEvents.noSignIn"
60410
60509
  }
60411
60510
  }
60412
60511
  ]
@@ -60837,7 +60936,11 @@ const SCREENS = runtimeSetting({
60837
60936
  "bottom": 24
60838
60937
  },
60839
60938
  "onAppear": {
60840
- "actionId": "tier5.himiMyEvents.load"
60939
+ "actionId": "tier5.himiMyEvents.load",
60940
+ "params": {
60941
+ "signInScreenId": "himi_sign_in",
60942
+ "memberOnly": "1"
60943
+ }
60841
60944
  }
60842
60945
  }
60843
60946
  },
@@ -99067,6 +99170,31 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
99067
99170
  "hiddenPath": "$service:himiBrandedProgram.hidePriceRetry"
99068
99171
  }
99069
99172
  },
99173
+ {
99174
+ "id": "himi_program.description_rich",
99175
+ "body": {
99176
+ "package": "himalaya",
99177
+ "type": "RichContent",
99178
+ "props": {
99179
+ "documentJsonPath": "$service:himiBrandedProgram.descriptionJson",
99180
+ "onLink": {
99181
+ "actionId": "tier5.himiBrandedProgram.openLink"
99182
+ },
99183
+ "fallback": {
99184
+ "id": "",
99185
+ "body": {
99186
+ "package": "himalaya",
99187
+ "type": "Text",
99188
+ "props": {
99189
+ "text": "$service:himiBrandedProgram.description",
99190
+ "style": "body",
99191
+ "color": "textSecondary"
99192
+ }
99193
+ }
99194
+ }
99195
+ }
99196
+ }
99197
+ },
99070
99198
  {
99071
99199
  "id": "",
99072
99200
  "body": {
@@ -99078,6 +99206,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
99078
99206
  "color": "textSecondary",
99079
99207
  "collapseWhenEmpty": true
99080
99208
  }
99209
+ },
99210
+ "modifiers": {
99211
+ "hiddenPath": "$service:himiBrandedProgram.hasDescriptionJson"
99081
99212
  }
99082
99213
  }
99083
99214
  ]
@@ -102873,6 +103004,9 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
102873
103004
  "style": "body",
102874
103005
  "color": "textSecondary"
102875
103006
  }
103007
+ },
103008
+ "modifiers": {
103009
+ "hiddenPath": "$service:himiBlogPost.signInRequired"
102876
103010
  }
102877
103011
  },
102878
103012
  {
@@ -102887,6 +103021,32 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
102887
103021
  "actionId": "tier5.himiBlogPost.retry"
102888
103022
  }
102889
103023
  }
103024
+ },
103025
+ "modifiers": {
103026
+ "hiddenPath": "$service:himiBlogPost.signInRequired"
103027
+ }
103028
+ },
103029
+ {
103030
+ "id": "himi_blog_post.retry.sign_in",
103031
+ "body": {
103032
+ "package": "himalaya",
103033
+ "type": "Button",
103034
+ "props": {
103035
+ "title": "Sign in",
103036
+ "style": "primary",
103037
+ "action": {
103038
+ "actionId": "himi.open",
103039
+ "params": {
103040
+ "screenId": "himi_sign_in",
103041
+ "mode": "signin",
103042
+ "modal": "1"
103043
+ },
103044
+ "presentation": "sheet"
103045
+ }
103046
+ }
103047
+ },
103048
+ "modifiers": {
103049
+ "hiddenPath": "$service:himiBlogPost.noSignIn"
102890
103050
  }
102891
103051
  }
102892
103052
  ]
@@ -103591,7 +103751,8 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
103591
103751
  "onAppear": {
103592
103752
  "actionId": "tier5.himiBlogPost.load",
103593
103753
  "params": {
103594
- "id": "__HIMI_ID__"
103754
+ "id": "__HIMI_ID__",
103755
+ "signInScreenId": "himi_sign_in"
103595
103756
  }
103596
103757
  }
103597
103758
  }
@@ -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,
@@ -25,6 +25,9 @@ import { offlineWriteMessage, withOnlineWrites } from "../_shared/online-actions
25
25
  import { connectivity, defineService as registerService } from "@himalaya/state";
26
26
  import { programImage } from "../_shared/himi-media.js";
27
27
  import { WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
28
+ import { descriptionBody } from "../_shared/rich-body.js";
29
+ import { programDescription } from "../_shared/program-description.js";
30
+ import { openRichLink } from "../_shared/rich-content-link.js";
28
31
  import {
29
32
  getProgram,
30
33
  joinProgram,
@@ -54,8 +57,10 @@ interface State {
54
57
  /** Outcome line for the Enrol CTA — joined / pending approval / payment / failure. */
55
58
  actionMessage: string;
56
59
  name: string;
57
- /** Full (untruncated) description, tags stripped. */
60
+ /** Full readable fallback, plus the bounded Ricos body when present. */
58
61
  description: string;
62
+ descriptionJson: string;
63
+ hasDescriptionJson: boolean;
59
64
  /** "N sections · M steps" / "Self-paced" — structure meta line. */
60
65
  meta: string;
61
66
  /** Formatted price when the payload carries one ("" for free/absent). */
@@ -101,6 +106,7 @@ function derive(state: State): State {
101
106
  const empty = state.status === "ready" && !state.name;
102
107
  return {
103
108
  ...state,
109
+ hasDescriptionJson: state.descriptionJson !== "",
104
110
  isLoading: state.status === "loading",
105
111
  notLoading: state.status !== "loading",
106
112
  isErrored: state.status === "error",
@@ -118,16 +124,6 @@ function titleOf(p: WixProgram): string {
118
124
  return cleanTitle(p.settings?.title ?? nested ?? p.title ?? p.details?.title) || copyParam("program.programFallbackName", "Program");
119
125
  }
120
126
 
121
- function detailsOf(p: WixProgram): string {
122
- const desc = (p as { description?: unknown }).description;
123
- const nested = desc && typeof desc === "object" ? (desc as { details?: string }).details : undefined;
124
- const flat = typeof desc === "string" ? desc : undefined;
125
- return (p.settings?.description ?? nested ?? flat ?? p.details?.description ?? "")
126
- .replace(/<[^>]+>/g, " ")
127
- .replace(/\s+/g, " ")
128
- .trim();
129
- }
130
-
131
127
  function metaOf(p: WixProgram): string {
132
128
  const summary = (p as { contentSummary?: { stepCount?: number; sectionCount?: number } }).contentSummary;
133
129
  const bits: string[] = [];
@@ -155,15 +151,17 @@ function priceOf(p: WixProgram): string {
155
151
  function ready(state: State, p: WixProgram | undefined): State {
156
152
  const stateOf = ((p?.state ?? p?.status ?? "") as string).toUpperCase();
157
153
  if (!p || stateOf === "DRAFT") {
158
- return derive({ ...state, status: "ready", name: "", description: "", meta: "", priceLine: "", image: "", initial: "", emptyText: copyParam("program.emptyText", "This program is no longer available.") });
154
+ return derive({ ...state, status: "ready", name: "", description: "", descriptionJson: "", meta: "", priceLine: "", image: "", initial: "", emptyText: copyParam("program.emptyText", "This program is no longer available.") });
159
155
  }
160
156
  const name = titleOf(p);
161
157
  const image = programImage(p, WIX_DETAIL_IMAGE);
158
+ const body = descriptionBody(programDescription(p), copyParam("program.descriptionFallback", "Follow this program at your own pace."));
162
159
  return derive({
163
160
  ...state,
164
161
  status: "ready",
165
162
  name,
166
- description: detailsOf(p) || copyParam("program.descriptionFallback", "Follow this program at your own pace."),
163
+ description: body.bodyText,
164
+ descriptionJson: body.bodyJson,
167
165
  meta: metaOf(p),
168
166
  priceLine: priceOf(p),
169
167
  image,
@@ -273,7 +271,7 @@ const pricing = commerce.pricing;
273
271
  const PRICING_NOT_HYDRATED = [
274
272
  "priceLine", "priceNote", "hidePriceRetry", "enrollDisabled", "enrollNote", "enrollHidden", "buyHidden", "buyLabel", "buyDisabled", "buyNote",
275
273
  ] as const;
276
- const CONTENT_KEYS = ["status", "name", "description", "meta", "image", "initial", "emptyText"] as const;
274
+ const CONTENT_KEYS = ["status", "name", "description", "descriptionJson", "meta", "image", "initial", "emptyText"] as const;
277
275
  type Content = Pick<State, (typeof CONTENT_KEYS)[number]>;
278
276
  const NO_PRICE: IapPriceFields = { priceText: "", priceNote: "", priceLoading: false, hidePriceRetry: true, actionDisabled: true, actionNote: "" };
279
277
  const NOT_APPLE: IapPurchaseFields = { appleRoute: false, accessReady: false, buyLabel: "", buyDisabled: true, buyBusy: false, buyNote: "" };
@@ -463,6 +461,8 @@ const INITIAL_STATE: State = derive({
463
461
  actionMessage: "",
464
462
  name: "",
465
463
  description: "",
464
+ descriptionJson: "",
465
+ hasDescriptionJson: false,
466
466
  meta: "",
467
467
  priceLine: "",
468
468
  image: "",
@@ -488,6 +488,7 @@ const INITIAL_STATE: State = derive({
488
488
  });
489
489
 
490
490
  type Actions = {
491
+ openLink: (state: State, arg?: unknown) => Promise<undefined>;
491
492
  load: (state: State, arg?: unknown) => Promise<State | undefined>;
492
493
  enroll: (state: State) => Promise<State | undefined>;
493
494
  buy: (state: State) => Promise<State | undefined>;
@@ -506,6 +507,7 @@ const svc = defineService<State, Actions>("himiBrandedProgram", {
506
507
  enroll: () => enroll(),
507
508
  buy,
508
509
  pricingPublish: pricedState,
510
+ openLink: async (_state, arg) => { await openRichLink(arg); return undefined; },
509
511
  pricingStart: pricedState,
510
512
  pricingRefresh: async () => { await priceProgram(); return undefined; },
511
513
  iapPoll: async () => { await commerce.poll(); void svc.pricingPublish(); return undefined; },
@@ -17,6 +17,8 @@ import { createIapCommerce } from "@himalaya/iap-checkout";
17
17
  import { programImage } from "../_shared/himi-media.js";
18
18
  import { cleanTitle, initials } from "../_shared/himi-text.js";
19
19
  import { copyParam, stashCopy } from "../_shared/himi-copy.js";
20
+ import { descriptionBody } from "../_shared/rich-body.js";
21
+ import { programDescription } from "../_shared/program-description.js";
20
22
  import { iapPriceCopy, resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
21
23
 
22
24
  const SERVICE = "himiBrandedPrograms";
@@ -76,13 +78,7 @@ function programTitle(p: WixProgram): string {
76
78
  }
77
79
 
78
80
  function programDetails(p: WixProgram): string {
79
- const desc = (p as { description?: unknown }).description;
80
- const nested = desc && typeof desc === "object" ? (desc as { details?: string }).details : undefined;
81
- const flat = typeof desc === "string" ? desc : undefined;
82
- const raw = (p.settings?.description ?? nested ?? flat ?? p.details?.description ?? "")
83
- .replace(/<[^>]+>/g, " ")
84
- .replace(/\s+/g, " ")
85
- .trim();
81
+ const raw = descriptionBody(programDescription(p)).bodyText.replace(/\s+/g, " ").trim();
86
82
  return raw.length > 140 ? `${raw.slice(0, 139).trimEnd()}…` : raw;
87
83
  }
88
84