@wix/himalaya-cli 0.1573.0 → 0.1575.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 (112) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/app-config.d.ts +16 -5
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/define-app.d.ts +6 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/message-format.d.ts +59 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/telemetry/wix-bi/src/index.d.ts +15 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/telemetry/wix-bi/src/oneapp.d.ts +90 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +3 -3
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/l10n.mjs +8 -3
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +3 -3
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-bi.mjs +165 -5
  10. package/dist/cli.mjs +5261 -2937
  11. package/dist/preview-assets.generated.bin +0 -0
  12. package/guides/designing-himi-screens.md +7 -1
  13. package/guides/hhp.md +2 -2
  14. package/guides/himi-json-output.md +53 -2
  15. package/guides/himi-mcp.md +1 -1
  16. package/guides/himi-test.md +3 -2
  17. package/guides/iap-products.md +1 -1
  18. package/guides/importing-oneapp-translations.md +115 -0
  19. package/guides/index.json +6 -0
  20. package/guides/one-app-section-mappings.md +16 -4
  21. package/guides/one-app-to-himi-app.md +167 -10
  22. package/guides/push-cli-onboarding.md +10 -0
  23. package/guides/qa-builds.md +21 -1
  24. package/guides/templates.md +3 -2
  25. package/guides/testing-himi-apps.md +1 -1
  26. package/package.json +3 -3
  27. package/templates/beauty-salon/dev/index.ts +96 -133
  28. package/templates/beauty-salon/tier5-src/_shared/exclusive-writes.ts +66 -0
  29. package/templates/beauty-salon/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  30. package/templates/beauty-salon-owner/dev/index.ts +27 -6
  31. package/templates/beauty-salon-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  32. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  33. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  34. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  35. package/templates/branded-lite/dev/index.ts +136 -142
  36. package/templates/branded-lite/l10n/en.json +570 -0
  37. package/templates/branded-lite/l10n/he.json +200 -0
  38. package/templates/branded-lite/tier5-src/_shared/business-chat-copy.ts +40 -0
  39. package/templates/branded-lite/tier5-src/_shared/business-chat-message-model.ts +1 -1
  40. package/templates/branded-lite/tier5-src/_shared/business-chat-model.ts +3 -2
  41. package/templates/branded-lite/tier5-src/_shared/business-chat-presenter.ts +26 -21
  42. package/templates/branded-lite/tier5-src/_shared/exclusive-writes.ts +66 -0
  43. package/templates/branded-lite/tier5-src/business-chat/index.ts +184 -63
  44. package/templates/branded-lite/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  45. package/templates/community-club/dev/index.ts +22 -6
  46. package/templates/community-club-owner/dev/index.ts +27 -6
  47. package/templates/community-club-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  48. package/templates/community-club-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  49. package/templates/community-club-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  50. package/templates/community-club-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  51. package/templates/events-meetups/dev/index.ts +22 -6
  52. package/templates/events-meetups-owner/dev/index.ts +25 -6
  53. package/templates/events-meetups-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  54. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  55. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  56. package/templates/gym-fitness/dev/index.ts +96 -133
  57. package/templates/gym-fitness/tier5-src/_shared/exclusive-writes.ts +66 -0
  58. package/templates/gym-fitness/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  59. package/templates/gym-fitness-owner/dev/index.ts +28 -6
  60. package/templates/gym-fitness-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  61. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  62. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  63. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  64. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  65. package/templates/himi-chat/dev/index.ts +22 -6
  66. package/templates/nonprofit-cause/dev/index.ts +22 -6
  67. package/templates/nonprofit-cause-owner/dev/index.ts +26 -6
  68. package/templates/nonprofit-cause-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  69. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  70. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  71. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  72. package/templates/owner-lite/dev/index.ts +28 -6
  73. package/templates/owner-lite/tier5-src/_shared/exclusive-writes.ts +66 -0
  74. package/templates/owner-lite/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  75. package/templates/owner-lite/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  76. package/templates/owner-lite/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  77. package/templates/owner-lite/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  78. package/templates/podcast-creator/dev/index.ts +22 -6
  79. package/templates/podcast-creator-owner/dev/index.ts +27 -6
  80. package/templates/podcast-creator-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  81. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  82. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  83. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  84. package/templates/portfolio-creative/dev/index.ts +96 -133
  85. package/templates/portfolio-creative/tier5-src/_shared/exclusive-writes.ts +66 -0
  86. package/templates/portfolio-creative/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  87. package/templates/portfolio-creative-owner/dev/index.ts +27 -6
  88. package/templates/portfolio-creative-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  89. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  90. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  91. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  92. package/templates/restaurant/dev/index.ts +22 -6
  93. package/templates/restaurant-owner/dev/index.ts +28 -6
  94. package/templates/restaurant-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  95. package/templates/restaurant-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  96. package/templates/restaurant-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  97. package/templates/restaurant-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  98. package/templates/restaurant-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
  99. package/templates/retail-store/dev/index.ts +96 -133
  100. package/templates/retail-store/tier5-src/_shared/exclusive-writes.ts +66 -0
  101. package/templates/retail-store/tier5-src/himi-branded-lite-cart/index.ts +18 -5
  102. package/templates/retail-store-owner/dev/index.ts +26 -6
  103. package/templates/retail-store-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  104. package/templates/retail-store-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  105. package/templates/retail-store-owner/tier5-src/himi-owner-lite-product-form/index.ts +5 -1
  106. package/templates/surface-showcase/dev/index.ts +22 -6
  107. package/templates/yoga-studio/dev/index.ts +22 -6
  108. package/templates/yoga-studio-owner/dev/index.ts +26 -6
  109. package/templates/yoga-studio-owner/tier5-src/_shared/exclusive-writes.ts +66 -0
  110. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-event-form/index.ts +5 -1
  111. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-post-form/index.ts +5 -1
  112. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-service-form/index.ts +5 -1
@@ -8,7 +8,9 @@ import { editorSubmitValue, queuedDocument, editorChanged, editorRunKeys, resetH
8
8
  import { ricosUploadService } from "@himalaya/wix-rich-content-editor";
9
9
  import { wixCall } from "@himalaya/wix-site-media";
10
10
  import { wixMediaUrl, WIX_DETAIL_IMAGE } from "../_shared/himi-store.js";
11
- import { defineService } from "@himalaya/state";
11
+ import { defineService as registerService } from "@himalaya/state";
12
+ import { withExclusiveWrites } from "../_shared/exclusive-writes.js";
13
+ const defineService = withExclusiveWrites(registerService, ["submit"]);
12
14
  import { getDraftPost, createDraftPost, updateDraftPost, type WixDraftPostWrite, type WixRichContent } from "@himalaya/wix-blog";
13
15
  import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback, ownerWriteError } from "../_shared/owner-load-error.js";
14
16
  import { DEMO_POSTS, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch } from "../_shared/owner-preview-demo.js";
@@ -53,6 +55,7 @@ interface PostFormState {
53
55
  noCover: boolean;
54
56
  isFeatured: string;
55
57
  status: "idle" | "loading" | "saving" | "saved" | "error";
58
+ writeBusy: boolean;
56
59
  error: string;
57
60
  savedMessage: string;
58
61
  /** WS-C — canonical detail target (`himi_owner_lite_post`); `savedId` rides
@@ -128,6 +131,7 @@ function shape(s: Partial<PostFormState>): PostFormState {
128
131
  noCover: !s.coverUrl,
129
132
  isFeatured: s.isFeatured ?? "false",
130
133
  status: s.status ?? "idle",
134
+ writeBusy: s.writeBusy ?? false,
131
135
  error: s.error ?? "",
132
136
  savedMessage: s.savedMessage ?? "",
133
137
  // WS-C — canonical detail target + the saved entity's id as a separate nav
@@ -6,7 +6,9 @@
6
6
  // the WRITE shape needs `priceData: { price: <number>, currency }`. Bound by the
7
7
  // generic buildForm descriptor ($service:himiOwnerLiteProductForm.*).
8
8
 
9
- import { defineService } from "@himalaya/state";
9
+ import { defineService as registerService } from "@himalaya/state";
10
+ import { withExclusiveWrites } from "../_shared/exclusive-writes.js";
11
+ const defineService = withExclusiveWrites(registerService, ["submit"]);
10
12
  import {
11
13
  getProduct,
12
14
  createProduct,
@@ -76,6 +78,7 @@ interface ProductFormState {
76
78
  descriptionNote: string;
77
79
  visible: string;
78
80
  status: "idle" | "loading" | "saving" | "saved" | "error";
81
+ writeBusy: boolean;
79
82
  error: string;
80
83
  savedMessage: string;
81
84
  /** WS-C — canonical detail target (`himi_owner_lite_product`); `savedId` rides
@@ -161,6 +164,7 @@ function shape(s: Partial<ProductFormState>): ProductFormState {
161
164
  descriptionNote: dMode === "locked" ? copyParam("ownerProductForm.richDescriptionNote", "This description has formatting or media. Edit it in your site dashboard to keep that content intact; name, price and SKU can be changed here.") : "",
162
165
  visible: s.visible ?? "true",
163
166
  status: s.status ?? "idle",
167
+ writeBusy: s.writeBusy ?? false,
164
168
  error: s.error ?? "",
165
169
  savedMessage: s.savedMessage ?? "",
166
170
  // WS-C — canonical detail target + the saved entity's id as a separate nav
@@ -5,7 +5,9 @@
5
5
  // updateService. Bound by buildForm ($service:himiOwnerLiteServiceForm.*).
6
6
 
7
7
  import { landsOnLive } from "../_shared/owner-editor-value.js";
8
- import { defineService } from "@himalaya/state";
8
+ import { defineService as registerService } from "@himalaya/state";
9
+ import { withExclusiveWrites } from "../_shared/exclusive-writes.js";
10
+ const defineService = withExclusiveWrites(registerService, ["submit"]);
9
11
  import { getService, updateService, type WixService } from "@himalaya/wix-bookings";
10
12
  import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback, ownerWriteError } from "../_shared/owner-load-error.js";
11
13
  import { DEMO_SERVICES, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch } from "../_shared/owner-preview-demo.js";
@@ -31,6 +33,7 @@ interface ServiceFormState {
31
33
  description: string;
32
34
  category: string;
33
35
  status: "idle" | "loading" | "saving" | "saved" | "error";
36
+ writeBusy: boolean;
34
37
  error: string;
35
38
  savedMessage: string;
36
39
  /** WS-C — canonical detail target (`himi_owner_lite_service`); `savedId` rides
@@ -88,6 +91,7 @@ function shape(s: Partial<ServiceFormState>): ServiceFormState {
88
91
  description: s.description ?? "",
89
92
  category: s.category ?? "",
90
93
  status: s.status ?? "idle",
94
+ writeBusy: s.writeBusy ?? false,
91
95
  error: s.error ?? "",
92
96
  savedMessage: s.savedMessage ?? "",
93
97
  // WS-C — canonical detail target + the saved entity's id as a separate nav
@@ -26773,15 +26773,31 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
26773
26773
 
26774
26774
  // Live seed-id enumeration: a published release serves only EXACT screens, so
26775
26775
  // `himi push` bakes each templated detail route's CURRENT catalog ids as exact
26776
- // screens via `seedIds`. Anonymous visitor rung against this app's own clientId;
26777
- // best-effort (offline build → no baked details, preview still live-resolves).
26776
+ // screens via `seedIds`. Anonymous visitor rung against this app's own clientId — or,
26777
+ // for an app bound through the One App visitor identity (`visitorSource: "one-app"`,
26778
+ // no client), that site's One App visitor instance. Best-effort (offline build → no
26779
+ // baked details, preview still live-resolves).
26780
+ async function liveSeedToken(WIX: string): Promise<string | undefined> {
26781
+ const auth = (config as { auth?: { config?: { clientId?: string; visitorSource?: string; metaSiteId?: string } } }).auth?.config;
26782
+ // A One App binding boots as the site's One App visitor, so the seed reads as that visitor too —
26783
+ // even beside a member-login clientId, and never repointed by a stray local WIX_CLIENT_ID.
26784
+ const oneApp = auth?.visitorSource === "one-app";
26785
+ const clientId = oneApp ? "" : process.env.WIX_CLIENT_ID ?? auth?.clientId ?? "";
26786
+ if (clientId && !clientId.startsWith("YOUR_")) {
26787
+ const mint = await fetch(WIX + "/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId, grantType: "anonymous" }) });
26788
+ return ((await mint.json()) as { access_token?: string }).access_token;
26789
+ }
26790
+ const msid = oneApp ? auth?.metaSiteId ?? "" : "";
26791
+ if (!/^[0-9a-fA-F-]{10,64}$/.test(msid)) return undefined;
26792
+ const mint = await fetch(`https://apps.wix.com/_api/one-app-session-web/v3/businesses/${msid}/signed-instance?signed_instance_type=Visitor`, { headers: { Accept: "application/json" }, redirect: "error" });
26793
+ const visitor = (await mint.json()) as { signedInstance?: string; signedInstanceType?: string };
26794
+ return visitor.signedInstanceType === "Visitor" ? visitor.signedInstance : undefined;
26795
+ }
26796
+
26778
26797
  async function liveSeedIds(spec: LiveSeed): Promise<string[]> {
26779
26798
  try {
26780
26799
  const WIX = "https://www.wixapis.com";
26781
- const clientId = process.env.WIX_CLIENT_ID ?? (config as { auth?: { config?: { clientId?: string } } }).auth?.config?.clientId ?? "";
26782
- if (!clientId || clientId.startsWith("YOUR_")) return [];
26783
- const mint = await fetch(WIX + "/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId, grantType: "anonymous" }) });
26784
- const token = ((await mint.json()) as { access_token?: string }).access_token;
26800
+ const token = await liveSeedToken(WIX);
26785
26801
  if (!token) return [];
26786
26802
  const res = await fetch(WIX + spec.path, { method: "POST", headers: { Authorization: token, "Content-Type": "application/json" }, body: JSON.stringify(spec.body) });
26787
26803
  const json = (await res.json()) as Record<string, unknown>;
@@ -82378,6 +82378,7 @@ const SCREENS = runtimeSetting({
82378
82378
  "title": "Create product",
82379
82379
  "style": "primary",
82380
82380
  "fullWidth": true,
82381
+ "disabledPath": "$service:himiOwnerLiteProductForm.writeBusy",
82381
82382
  "action": {
82382
82383
  "actionId": "tier5.himiOwnerLiteProductForm.submit"
82383
82384
  }
@@ -82698,6 +82699,7 @@ const SCREENS = runtimeSetting({
82698
82699
  "title": "Create post",
82699
82700
  "style": "primary",
82700
82701
  "fullWidth": true,
82702
+ "disabledPath": "$service:himiOwnerLitePostForm.writeBusy",
82701
82703
  "action": {
82702
82704
  "actionId": "tier5.himiOwnerLitePostForm.submit"
82703
82705
  }
@@ -83213,6 +83215,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
83213
83215
  "title": "Save changes",
83214
83216
  "style": "primary",
83215
83217
  "fullWidth": true,
83218
+ "disabledPath": "$service:himiOwnerLiteProductForm.writeBusy",
83216
83219
  "action": {
83217
83220
  "actionId": "tier5.himiOwnerLiteProductForm.submit"
83218
83221
  }
@@ -83461,6 +83464,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
83461
83464
  "title": "Save changes",
83462
83465
  "style": "primary",
83463
83466
  "fullWidth": true,
83467
+ "disabledPath": "$service:himiOwnerLiteServiceForm.writeBusy",
83464
83468
  "action": {
83465
83469
  "actionId": "tier5.himiOwnerLiteServiceForm.submit"
83466
83470
  }
@@ -83823,6 +83827,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
83823
83827
  "title": "Save changes",
83824
83828
  "style": "primary",
83825
83829
  "fullWidth": true,
83830
+ "disabledPath": "$service:himiOwnerLiteEventForm.writeBusy",
83826
83831
  "action": {
83827
83832
  "actionId": "tier5.himiOwnerLiteEventForm.submit"
83828
83833
  }
@@ -84146,6 +84151,7 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
84146
84151
  "title": "Save changes",
84147
84152
  "style": "primary",
84148
84153
  "fullWidth": true,
84154
+ "disabledPath": "$service:himiOwnerLitePostForm.writeBusy",
84149
84155
  "action": {
84150
84156
  "actionId": "tier5.himiOwnerLitePostForm.submit"
84151
84157
  }
@@ -90719,15 +90725,31 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
90719
90725
 
90720
90726
  // Live seed-id enumeration: a published release serves only EXACT screens, so
90721
90727
  // `himi push` bakes each templated detail route's CURRENT catalog ids as exact
90722
- // screens via `seedIds`. Anonymous visitor rung against this app's own clientId;
90723
- // best-effort (offline build → no baked details, preview still live-resolves).
90728
+ // screens via `seedIds`. Anonymous visitor rung against this app's own clientId — or,
90729
+ // for an app bound through the One App visitor identity (`visitorSource: "one-app"`,
90730
+ // no client), that site's One App visitor instance. Best-effort (offline build → no
90731
+ // baked details, preview still live-resolves).
90732
+ async function liveSeedToken(WIX: string): Promise<string | undefined> {
90733
+ const auth = (config as { auth?: { config?: { clientId?: string; visitorSource?: string; metaSiteId?: string } } }).auth?.config;
90734
+ // A One App binding boots as the site's One App visitor, so the seed reads as that visitor too —
90735
+ // even beside a member-login clientId, and never repointed by a stray local WIX_CLIENT_ID.
90736
+ const oneApp = auth?.visitorSource === "one-app";
90737
+ const clientId = oneApp ? "" : process.env.WIX_CLIENT_ID ?? auth?.clientId ?? "";
90738
+ if (clientId && !clientId.startsWith("YOUR_")) {
90739
+ const mint = await fetch(WIX + "/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId, grantType: "anonymous" }) });
90740
+ return ((await mint.json()) as { access_token?: string }).access_token;
90741
+ }
90742
+ const msid = oneApp ? auth?.metaSiteId ?? "" : "";
90743
+ if (!/^[0-9a-fA-F-]{10,64}$/.test(msid)) return undefined;
90744
+ const mint = await fetch(`https://apps.wix.com/_api/one-app-session-web/v3/businesses/${msid}/signed-instance?signed_instance_type=Visitor`, { headers: { Accept: "application/json" }, redirect: "error" });
90745
+ const visitor = (await mint.json()) as { signedInstance?: string; signedInstanceType?: string };
90746
+ return visitor.signedInstanceType === "Visitor" ? visitor.signedInstance : undefined;
90747
+ }
90748
+
90724
90749
  async function liveSeedIds(spec: LiveSeed): Promise<string[]> {
90725
90750
  try {
90726
90751
  const WIX = "https://www.wixapis.com";
90727
- const clientId = process.env.WIX_CLIENT_ID ?? (config as { auth?: { config?: { clientId?: string } } }).auth?.config?.clientId ?? "";
90728
- if (!clientId || clientId.startsWith("YOUR_")) return [];
90729
- const mint = await fetch(WIX + "/oauth2/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId, grantType: "anonymous" }) });
90730
- const token = ((await mint.json()) as { access_token?: string }).access_token;
90752
+ const token = await liveSeedToken(WIX);
90731
90753
  if (!token) return [];
90732
90754
  const res = await fetch(WIX + spec.path, { method: "POST", headers: { Authorization: token, "Content-Type": "application/json" }, body: JSON.stringify(spec.body) });
90733
90755
  const json = (await res.json()) as Record<string, unknown>;
@@ -0,0 +1,66 @@
1
+ import { defineService as register, type ServiceDefinition } from '@himalaya/state';
2
+
3
+ /** Reject repeated taps until the accepted write's result is committed. A promise
4
+ * queue alone releases before the host applies the reducer result, so the next
5
+ * write can still read an old revision/id. The private synchronous reducer below
6
+ * publishes busy and commits completion through the ordinary service handle;
7
+ * awaiting that handle guarantees the host has stored the result before unlock.
8
+ * Skipped actions return undefined, so they cannot roll back the accepted write.
9
+ */
10
+ export function withExclusiveWrites<S extends object = Record<string, unknown>>(
11
+ registerService: typeof register, names: readonly string[],
12
+ options: { prepare?: (action: string, args: readonly unknown[]) => void; coalesce?: readonly string[]; settle?: (before: S, next: S, live: S) => S } = {},
13
+ ): typeof register {
14
+ return ((name: string, definition: ServiceDefinition<Record<string, unknown>>) => {
15
+ let busy = false;
16
+ let activeAction = "";
17
+ let completion = Promise.resolve();
18
+ let complete = () => {};
19
+ let handle: { getState(): Record<string, unknown>; exclusiveWriteState(next: Record<string, unknown>): Promise<unknown> };
20
+ const wrapped: ServiceDefinition<Record<string, unknown>> = {
21
+ ...definition,
22
+ state: { ...definition.state, writeBusy: false },
23
+ notHydrated: [...(definition.notHydrated ?? []), 'writeBusy'],
24
+ actions: {
25
+ ...Object.fromEntries(Object.entries(definition.actions ?? {}).map(([action, reduce]) => [action,
26
+ names.includes(action) ? async (state: Record<string, unknown>, ...args: never[]) => {
27
+ // Intake request metadata even when its state replacement is skipped.
28
+ options.prepare?.(action, args);
29
+ if (busy) {
30
+ // Repeated initial loads must await the accepted load, so callers
31
+ // observe its committed rows rather than the initial skeleton.
32
+ if (activeAction === action && options.coalesce?.includes(action)) await completion;
33
+ return;
34
+ }
35
+ busy = true;
36
+ activeAction = action;
37
+ completion = new Promise<void>(resolve => { complete = resolve; });
38
+ try {
39
+ await handle.exclusiveWriteState({ ...handle.getState(), writeBusy: true });
40
+ const next = await reduce(state, ...args);
41
+ const live = handle.getState();
42
+ // Non-write actions (for example editing a draft field) can still run
43
+ // during I/O. Let the owning service preserve those newer edits.
44
+ const settled = options.settle ? options.settle(state as S, (next ?? live) as S, live as S) : next ?? live;
45
+ await handle.exclusiveWriteState({ ...settled, writeBusy: false });
46
+ } catch (error) {
47
+ await handle.exclusiveWriteState({ ...handle.getState(), writeBusy: false });
48
+ throw error;
49
+ } finally {
50
+ busy = false;
51
+ complete();
52
+ }
53
+ } : reduce,
54
+ ])),
55
+ exclusiveWriteState: (_state, next: Record<string, unknown>) => next,
56
+ },
57
+ };
58
+ const registered = registerService(name, wrapped);
59
+ // Registration preserves this private action, but the dynamic action map
60
+ // cannot express its payload to the generic service handle.
61
+ handle = registered as unknown as typeof handle;
62
+ // Older hosts ignore notHydrated. A fresh worker has no pending write.
63
+ handle.getState().writeBusy = false;
64
+ return registered;
65
+ }) as typeof register;
66
+ }
@@ -6,7 +6,9 @@
6
6
  // form can't safely collect — so this form only edits an existing event's text
7
7
  // (title + description). Bound by buildForm ($service:himiOwnerLiteEventForm.*).
8
8
 
9
- import { defineService } from "@himalaya/state";
9
+ import { defineService as registerService } from "@himalaya/state";
10
+ import { withExclusiveWrites } from "../_shared/exclusive-writes.js";
11
+ const defineService = withExclusiveWrites(registerService, ["submit"]);
10
12
  import { getEvent, updateEvent, type WixEvent } from "@himalaya/wix-events";
11
13
  import { normalizeEvent } from "../_shared/himi-events.js";
12
14
  import { postBodyText, plainPostBody } from "../_shared/owner-post-content.js";
@@ -66,6 +68,7 @@ interface EventFormState {
66
68
  when: string;
67
69
  location: string;
68
70
  status: "idle" | "loading" | "saving" | "saved" | "error";
71
+ writeBusy: boolean;
69
72
  error: string;
70
73
  savedMessage: string;
71
74
  /** WS-C — canonical detail target (`himi_owner_lite_event`); `savedId` rides
@@ -140,6 +143,7 @@ function shape(s: Partial<EventFormState>): EventFormState {
140
143
  when: s.when ?? "—",
141
144
  location: s.location ?? "—",
142
145
  status: s.status ?? "idle",
146
+ writeBusy: s.writeBusy ?? false,
143
147
  error: s.error ?? "",
144
148
  savedMessage: s.savedMessage ?? "",
145
149
  // WS-C — canonical detail target + the saved entity's id as a separate nav
@@ -8,7 +8,9 @@ import { editorSubmitValue, queuedDocument, editorChanged, editorRunKeys, resetH
8
8
  import { ricosUploadService } from "@himalaya/wix-rich-content-editor";
9
9
  import { wixCall } from "@himalaya/wix-site-media";
10
10
  import { wixMediaUrl, WIX_DETAIL_IMAGE } from "../_shared/himi-store.js";
11
- import { defineService } from "@himalaya/state";
11
+ import { defineService as registerService } from "@himalaya/state";
12
+ import { withExclusiveWrites } from "../_shared/exclusive-writes.js";
13
+ const defineService = withExclusiveWrites(registerService, ["submit"]);
12
14
  import { getDraftPost, createDraftPost, updateDraftPost, type WixDraftPostWrite, type WixRichContent } from "@himalaya/wix-blog";
13
15
  import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback, ownerWriteError } from "../_shared/owner-load-error.js";
14
16
  import { DEMO_POSTS, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch } from "../_shared/owner-preview-demo.js";
@@ -53,6 +55,7 @@ interface PostFormState {
53
55
  noCover: boolean;
54
56
  isFeatured: string;
55
57
  status: "idle" | "loading" | "saving" | "saved" | "error";
58
+ writeBusy: boolean;
56
59
  error: string;
57
60
  savedMessage: string;
58
61
  /** WS-C — canonical detail target (`himi_owner_lite_post`); `savedId` rides
@@ -128,6 +131,7 @@ function shape(s: Partial<PostFormState>): PostFormState {
128
131
  noCover: !s.coverUrl,
129
132
  isFeatured: s.isFeatured ?? "false",
130
133
  status: s.status ?? "idle",
134
+ writeBusy: s.writeBusy ?? false,
131
135
  error: s.error ?? "",
132
136
  savedMessage: s.savedMessage ?? "",
133
137
  // WS-C — canonical detail target + the saved entity's id as a separate nav
@@ -6,7 +6,9 @@
6
6
  // the WRITE shape needs `priceData: { price: <number>, currency }`. Bound by the
7
7
  // generic buildForm descriptor ($service:himiOwnerLiteProductForm.*).
8
8
 
9
- import { defineService } from "@himalaya/state";
9
+ import { defineService as registerService } from "@himalaya/state";
10
+ import { withExclusiveWrites } from "../_shared/exclusive-writes.js";
11
+ const defineService = withExclusiveWrites(registerService, ["submit"]);
10
12
  import {
11
13
  getProduct,
12
14
  createProduct,
@@ -76,6 +78,7 @@ interface ProductFormState {
76
78
  descriptionNote: string;
77
79
  visible: string;
78
80
  status: "idle" | "loading" | "saving" | "saved" | "error";
81
+ writeBusy: boolean;
79
82
  error: string;
80
83
  savedMessage: string;
81
84
  /** WS-C — canonical detail target (`himi_owner_lite_product`); `savedId` rides
@@ -161,6 +164,7 @@ function shape(s: Partial<ProductFormState>): ProductFormState {
161
164
  descriptionNote: dMode === "locked" ? copyParam("ownerProductForm.richDescriptionNote", "This description has formatting or media. Edit it in your site dashboard to keep that content intact; name, price and SKU can be changed here.") : "",
162
165
  visible: s.visible ?? "true",
163
166
  status: s.status ?? "idle",
167
+ writeBusy: s.writeBusy ?? false,
164
168
  error: s.error ?? "",
165
169
  savedMessage: s.savedMessage ?? "",
166
170
  // WS-C — canonical detail target + the saved entity's id as a separate nav
@@ -5,7 +5,9 @@
5
5
  // updateService. Bound by buildForm ($service:himiOwnerLiteServiceForm.*).
6
6
 
7
7
  import { landsOnLive } from "../_shared/owner-editor-value.js";
8
- import { defineService } from "@himalaya/state";
8
+ import { defineService as registerService } from "@himalaya/state";
9
+ import { withExclusiveWrites } from "../_shared/exclusive-writes.js";
10
+ const defineService = withExclusiveWrites(registerService, ["submit"]);
9
11
  import { getService, updateService, type WixService } from "@himalaya/wix-bookings";
10
12
  import { ownerLoadError, isOwnerPreviewDemo, ownerDemoFallback, ownerWriteError } from "../_shared/owner-load-error.js";
11
13
  import { DEMO_SERVICES, DEMO_NOTE, DEMO_SAVE_NOTE, demoRefusalPatch } from "../_shared/owner-preview-demo.js";
@@ -31,6 +33,7 @@ interface ServiceFormState {
31
33
  description: string;
32
34
  category: string;
33
35
  status: "idle" | "loading" | "saving" | "saved" | "error";
36
+ writeBusy: boolean;
34
37
  error: string;
35
38
  savedMessage: string;
36
39
  /** WS-C — canonical detail target (`himi_owner_lite_service`); `savedId` rides
@@ -88,6 +91,7 @@ function shape(s: Partial<ServiceFormState>): ServiceFormState {
88
91
  description: s.description ?? "",
89
92
  category: s.category ?? "",
90
93
  status: s.status ?? "idle",
94
+ writeBusy: s.writeBusy ?? false,
91
95
  error: s.error ?? "",
92
96
  savedMessage: s.savedMessage ?? "",
93
97
  // WS-C — canonical detail target + the saved entity's id as a separate nav