@wix/himalaya-cli 0.1526.0 → 0.1528.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/state/src/connectivity.d.ts +5 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/state/src/index.d.ts +1 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/state.mjs +14 -0
  4. package/dist/cli.mjs +3633 -3260
  5. package/dist/preview-assets.generated.bin +0 -0
  6. package/guides/himi-json-output.md +45 -0
  7. package/guides/himi-mcp.md +41 -0
  8. package/guides/himi-test.md +3 -3
  9. package/guides/iap-products.md +88 -23
  10. package/package.json +2 -2
  11. package/templates/beauty-salon/dev/index.ts +2027 -759
  12. package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +5 -1
  13. package/templates/beauty-salon/tier5-src/_shared/online-actions.ts +76 -0
  14. package/templates/beauty-salon/tier5-src/himi-booking-bookings/index.ts +1 -0
  15. package/templates/beauty-salon/tier5-src/himi-booking-time/index.ts +803 -284
  16. package/templates/beauty-salon/tier5-src/himi-branded-lite-address/index.ts +3 -1
  17. package/templates/beauty-salon/tier5-src/himi-branded-lite-addresses/index.ts +3 -1
  18. package/templates/beauty-salon/tier5-src/himi-branded-lite-cart/index.ts +3 -0
  19. package/templates/beauty-salon/tier5-src/himi-branded-lite-order/index.ts +3 -1
  20. package/templates/beauty-salon/tier5-src/himi-branded-lite-plans/index.ts +13 -1
  21. package/templates/beauty-salon/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
  22. package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +10 -4
  23. package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +5 -1
  24. package/templates/beauty-salon-owner/tier5-src/_shared/online-actions.ts +76 -0
  25. package/templates/branded-lite/dev/index.ts +1910 -603
  26. package/templates/branded-lite/tier5-src/_shared/collection-service.ts +5 -1
  27. package/templates/branded-lite/tier5-src/_shared/online-actions.ts +76 -0
  28. package/templates/branded-lite/tier5-src/himi-booking-bookings/index.ts +1 -0
  29. package/templates/branded-lite/tier5-src/himi-booking-time/index.ts +803 -284
  30. package/templates/branded-lite/tier5-src/himi-branded-lite-address/index.ts +3 -1
  31. package/templates/branded-lite/tier5-src/himi-branded-lite-addresses/index.ts +3 -1
  32. package/templates/branded-lite/tier5-src/himi-branded-lite-cart/index.ts +3 -0
  33. package/templates/branded-lite/tier5-src/himi-branded-lite-feed/index.ts +3 -1
  34. package/templates/branded-lite/tier5-src/himi-branded-lite-group/index.ts +3 -1
  35. package/templates/branded-lite/tier5-src/himi-branded-lite-notifications/index.ts +6 -4
  36. package/templates/branded-lite/tier5-src/himi-branded-lite-notifsettings/index.ts +6 -4
  37. package/templates/branded-lite/tier5-src/himi-branded-lite-order/index.ts +3 -1
  38. package/templates/branded-lite/tier5-src/himi-branded-lite-plans/index.ts +13 -1
  39. package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +3 -1
  40. package/templates/branded-lite/tier5-src/himi-branded-lite-reservations/index.ts +6 -4
  41. package/templates/branded-lite/tier5-src/himi-branded-lite-reserve/index.ts +3 -1
  42. package/templates/branded-lite/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
  43. package/templates/branded-lite/tier5-src/himi-events-detail/index.ts +8 -1
  44. package/templates/branded-lite/tier5-src/himi-events-rsvp/index.ts +7 -4
  45. package/templates/branded-lite/tier5-src/himi-store-product/index.ts +10 -4
  46. package/templates/community-club/tier5-src/_shared/collection-service.ts +5 -1
  47. package/templates/community-club/tier5-src/_shared/online-actions.ts +76 -0
  48. package/templates/community-club/tier5-src/himi-branded-lite-feed/index.ts +3 -1
  49. package/templates/community-club/tier5-src/himi-branded-lite-group/index.ts +3 -1
  50. package/templates/community-club/tier5-src/himi-events-detail/index.ts +8 -1
  51. package/templates/community-club/tier5-src/himi-events-rsvp/index.ts +7 -4
  52. package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +5 -1
  53. package/templates/community-club-owner/tier5-src/_shared/online-actions.ts +76 -0
  54. package/templates/events-meetups/tier5-src/_shared/collection-service.ts +5 -1
  55. package/templates/events-meetups/tier5-src/_shared/online-actions.ts +76 -0
  56. package/templates/events-meetups/tier5-src/himi-branded-lite-feed/index.ts +3 -1
  57. package/templates/events-meetups/tier5-src/himi-branded-lite-group/index.ts +3 -1
  58. package/templates/events-meetups/tier5-src/himi-events-detail/index.ts +8 -1
  59. package/templates/events-meetups/tier5-src/himi-events-rsvp/index.ts +7 -4
  60. package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +5 -1
  61. package/templates/events-meetups-owner/tier5-src/_shared/online-actions.ts +76 -0
  62. package/templates/gym-fitness/dev/index.ts +2024 -756
  63. package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +5 -1
  64. package/templates/gym-fitness/tier5-src/_shared/online-actions.ts +76 -0
  65. package/templates/gym-fitness/tier5-src/himi-booking-bookings/index.ts +1 -0
  66. package/templates/gym-fitness/tier5-src/himi-booking-time/index.ts +803 -284
  67. package/templates/gym-fitness/tier5-src/himi-branded-lite-address/index.ts +3 -1
  68. package/templates/gym-fitness/tier5-src/himi-branded-lite-addresses/index.ts +3 -1
  69. package/templates/gym-fitness/tier5-src/himi-branded-lite-cart/index.ts +3 -0
  70. package/templates/gym-fitness/tier5-src/himi-branded-lite-order/index.ts +3 -1
  71. package/templates/gym-fitness/tier5-src/himi-branded-lite-plans/index.ts +13 -1
  72. package/templates/gym-fitness/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
  73. package/templates/gym-fitness/tier5-src/himi-events-detail/index.ts +8 -1
  74. package/templates/gym-fitness/tier5-src/himi-events-rsvp/index.ts +7 -4
  75. package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +10 -4
  76. package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +5 -1
  77. package/templates/gym-fitness-owner/tier5-src/_shared/online-actions.ts +76 -0
  78. package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +5 -1
  79. package/templates/nonprofit-cause/tier5-src/_shared/online-actions.ts +76 -0
  80. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-feed/index.ts +3 -1
  81. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-group/index.ts +3 -1
  82. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-plans/index.ts +13 -1
  83. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
  84. package/templates/nonprofit-cause/tier5-src/himi-events-detail/index.ts +8 -1
  85. package/templates/nonprofit-cause/tier5-src/himi-events-rsvp/index.ts +7 -4
  86. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +5 -1
  87. package/templates/nonprofit-cause-owner/tier5-src/_shared/online-actions.ts +76 -0
  88. package/templates/owner-lite/tier5-src/_shared/collection-service.ts +5 -1
  89. package/templates/owner-lite/tier5-src/_shared/online-actions.ts +76 -0
  90. package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +5 -1
  91. package/templates/podcast-creator/tier5-src/_shared/online-actions.ts +76 -0
  92. package/templates/podcast-creator/tier5-src/himi-branded-lite-feed/index.ts +3 -1
  93. package/templates/podcast-creator/tier5-src/himi-branded-lite-plans/index.ts +13 -1
  94. package/templates/podcast-creator/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
  95. package/templates/podcast-creator/tier5-src/himi-events-detail/index.ts +8 -1
  96. package/templates/podcast-creator/tier5-src/himi-events-rsvp/index.ts +7 -4
  97. package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +5 -1
  98. package/templates/podcast-creator-owner/tier5-src/_shared/online-actions.ts +76 -0
  99. package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +5 -1
  100. package/templates/portfolio-creative/tier5-src/_shared/online-actions.ts +76 -0
  101. package/templates/portfolio-creative/tier5-src/himi-branded-lite-address/index.ts +3 -1
  102. package/templates/portfolio-creative/tier5-src/himi-branded-lite-addresses/index.ts +3 -1
  103. package/templates/portfolio-creative/tier5-src/himi-branded-lite-cart/index.ts +3 -0
  104. package/templates/portfolio-creative/tier5-src/himi-branded-lite-order/index.ts +3 -1
  105. package/templates/portfolio-creative/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
  106. package/templates/portfolio-creative/tier5-src/himi-events-detail/index.ts +8 -1
  107. package/templates/portfolio-creative/tier5-src/himi-events-rsvp/index.ts +7 -4
  108. package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +10 -4
  109. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +5 -1
  110. package/templates/portfolio-creative-owner/tier5-src/_shared/online-actions.ts +76 -0
  111. package/templates/restaurant/tier5-src/_shared/collection-service.ts +5 -1
  112. package/templates/restaurant/tier5-src/_shared/online-actions.ts +76 -0
  113. package/templates/restaurant/tier5-src/himi-branded-lite-address/index.ts +3 -1
  114. package/templates/restaurant/tier5-src/himi-branded-lite-addresses/index.ts +3 -1
  115. package/templates/restaurant/tier5-src/himi-branded-lite-reserve/index.ts +3 -1
  116. package/templates/restaurant/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
  117. package/templates/restaurant/tier5-src/himi-events-detail/index.ts +8 -1
  118. package/templates/restaurant/tier5-src/himi-events-rsvp/index.ts +7 -4
  119. package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +5 -1
  120. package/templates/restaurant-owner/tier5-src/_shared/online-actions.ts +76 -0
  121. package/templates/retail-store/tier5-src/_shared/collection-service.ts +5 -1
  122. package/templates/retail-store/tier5-src/_shared/online-actions.ts +76 -0
  123. package/templates/retail-store/tier5-src/himi-branded-lite-address/index.ts +3 -1
  124. package/templates/retail-store/tier5-src/himi-branded-lite-addresses/index.ts +3 -1
  125. package/templates/retail-store/tier5-src/himi-branded-lite-cart/index.ts +3 -0
  126. package/templates/retail-store/tier5-src/himi-branded-lite-order/index.ts +3 -1
  127. package/templates/retail-store/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
  128. package/templates/retail-store/tier5-src/himi-store-product/index.ts +10 -4
  129. package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +5 -1
  130. package/templates/retail-store-owner/tier5-src/_shared/online-actions.ts +76 -0
  131. package/templates/yoga-studio/dev/index.ts +2054 -786
  132. package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +5 -1
  133. package/templates/yoga-studio/tier5-src/_shared/online-actions.ts +76 -0
  134. package/templates/yoga-studio/tier5-src/himi-booking-bookings/index.ts +1 -0
  135. package/templates/yoga-studio/tier5-src/himi-booking-time/index.ts +803 -284
  136. package/templates/yoga-studio/tier5-src/himi-branded-lite-plans/index.ts +13 -1
  137. package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +3 -1
  138. package/templates/yoga-studio/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
  139. package/templates/yoga-studio/tier5-src/himi-events-detail/index.ts +8 -1
  140. package/templates/yoga-studio/tier5-src/himi-events-rsvp/index.ts +7 -4
  141. package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +5 -1
  142. package/templates/yoga-studio-owner/tier5-src/_shared/online-actions.ts +76 -0
@@ -1,9 +1,10 @@
1
+ import { withOnlineWrites } from "../_shared/online-actions.js";
1
2
  // Tier-5 bundle: Branded Lite — add / edit one saved address (himi_address).
2
3
  //
3
4
  // No `id` nav param → a new address; `?id=` → that address, prefilled. Save writes it
4
5
  // to the member's address book (@himalaya/wix-ecom) and goes back to My Addresses,
5
6
  // which reloads on appear. Edit also offers Remove, behind an in-screen confirm.
6
- import { defineService, navigate } from "@himalaya/state";
7
+ import { defineService as registerService, navigate } from "@himalaya/state";
7
8
  import {
8
9
  createMemberAddress,
9
10
  deleteMemberAddress,
@@ -176,6 +177,7 @@ async function remove(state: State): Promise<State> {
176
177
  }
177
178
  }
178
179
 
180
+ const defineService = withOnlineWrites(registerService, ["save", "remove"], { derive });
179
181
  const svc = defineService<State, {
180
182
  load: (state: State, arg?: unknown) => Promise<State>;
181
183
  retry: (state: State) => Promise<State>;
@@ -1,10 +1,11 @@
1
+ import { withOnlineWrites } from "../_shared/online-actions.js";
1
2
  // Tier-5 bundle: Branded Lite — My Addresses (the member's saved address book).
2
3
  //
3
4
  // The same book the site's Members Area "My Addresses" page and checkout use
4
5
  // (addresses-web, see @himalaya/wix-ecom listMemberAddresses). A member sees every
5
6
  // saved address, picks the default, and removes one after an in-screen confirm;
6
7
  // add/edit open himi_address. A guest gets the shared member gate.
7
- import { defineService, navigate } from "@himalaya/state";
8
+ import { defineService as registerService, navigate } from "@himalaya/state";
8
9
  import {
9
10
  deleteMemberAddress,
10
11
  listMemberAddresses,
@@ -152,6 +153,7 @@ async function write(state: State, run: () => Promise<unknown>, done: string, fa
152
153
  }
153
154
  }
154
155
 
156
+ const defineService = withOnlineWrites(registerService, ["setDefault", "remove"], { derive });
155
157
  const svc = defineService<State, {
156
158
  load: (state: State, arg?: unknown) => Promise<State>;
157
159
  retry: (state: State) => Promise<State>;
@@ -1,3 +1,5 @@
1
+ import { withOnlineWrites } from "../_shared/online-actions.js";
2
+ import { defineService as registerState } from "@himalaya/state";
1
3
  import { getStoreProduct } from "../_shared/himi-store.js";
2
4
  import { iapPriceCopy } from "../_shared/iap-worker.js";
3
5
  import { withIapPresentation } from "../_shared/iap-presentation.js";
@@ -378,6 +380,7 @@ function couponDetails(result: unknown): { id: string; code: string } {
378
380
 
379
381
  let cartPricing: IapCommerce;
380
382
  const defineService = withIapPresentation<State>({
383
+ register: withOnlineWrites(registerState, ["remove", "increment", "decrement", "checkout", "beginPromo", "applyPromo", "removePromo"], { derive, messageFields: action => action.includes("Promo") ? ["promoError"] : ["checkoutHint"] }),
381
384
  onCreate: commerce => { cartPricing = commerce; },
382
385
  prefix: "cart", transientKeys: ["items", "subtotal", "promoBusy"], contentActions: ["load", "remove", "increment", "decrement"],
383
386
  items: state => state.items.flatMap(row => row.catalogReference?.catalogItemId ? [{ reference: { kind: "storesProduct", id: row.catalogReference.catalogItemId }, commerceDisplayPrice: row.commerceTotal ?? row.total }] : []),
@@ -1,3 +1,4 @@
1
+ import { withOnlineWrites } from "../_shared/online-actions.js";
1
2
  // Tier-5 bundle: himi-branded-lite — community activity feed (design §4.5).
2
3
  //
3
4
  // The feed rides the Wix Groups social feed: a feed item lives in a group
@@ -9,7 +10,7 @@
9
10
  // worker's signed-in flags (the himi_group join-button pattern) and the
10
11
  // sign-in card routes to himi_account — never a dead tap.
11
12
  // Four-state (loading / error / rows / empty) like the sibling branded workers.
12
- import { defineService } from "@himalaya/state";
13
+ import { defineService as registerService } from "@himalaya/state";
13
14
  import {
14
15
  createFeedItem,
15
16
  listFeedItems,
@@ -301,6 +302,7 @@ async function loadFeed(state: State): Promise<State> {
301
302
  }
302
303
  }
303
304
 
305
+ const defineService = withOnlineWrites(registerService, ["post", "react"], { derive, draftFields: { draft: "message" } });
304
306
  const svc = defineService<State, {
305
307
  load: (state: State, arg?: unknown) => Promise<State>;
306
308
  filter: (state: State, arg?: unknown) => State;
@@ -1,4 +1,5 @@
1
- import { defineService } from "@himalaya/state";
1
+ import { withOnlineWrites } from "../_shared/online-actions.js";
2
+ import { defineService as registerService } from "@himalaya/state";
2
3
  import { getGroup, joinGroup, leaveGroup, queryMembers as queryGroupMembers, type WixGroup } from "@himalaya/wix-groups";
3
4
  import { getMember } from "@himalaya/wix-members";
4
5
  import { signedInEmail } from "../_shared/himi-account";
@@ -159,6 +160,7 @@ const initial: State = {
159
160
  hideContent: true,
160
161
  };
161
162
 
163
+ const defineService = withOnlineWrites(registerService, ["join","leave"]);
162
164
  const svc = defineService<State, {
163
165
  load: (state: State, arg?: unknown) => Promise<State>;
164
166
  join: (state: State) => Promise<State>;
@@ -1,3 +1,4 @@
1
+ import { withOnlineWrites } from "../_shared/online-actions.js";
1
2
  // himi-branded-lite-notifications — reads the member's notifications via Members Feed Viewer (queryNotifications).
2
3
  // a failed query is an error with Retry (or Sign in when Wix refuses the
3
4
  // visitor) and an empty one is a real empty state — never sample rows presented as
@@ -5,7 +6,7 @@
5
6
  // (_shared/branded-load-state.ts).
6
7
  // mark-one-read / mark-all-read stay wired: they update state optimistically and
7
8
  // best-effort the real write (a no-op on sample rows).
8
- import { defineService } from "../_shared/collection-service.js";
9
+ import { defineService as registerService } from "../_shared/collection-service.js";
9
10
  import { LOADING, loaded, loadFailed, type LoadCopy, type LoadFlags } from "../_shared/branded-load-state.js";
10
11
  import { queryNotifications, markRead, markAllRead, type MfvNotification } from "@himalaya/wix-members-feed-viewer";
11
12
 
@@ -22,7 +23,7 @@ interface Row {
22
23
  dest: string;
23
24
  }
24
25
  interface S extends LoadFlags {
25
- items: Row[]; emptyText: string;
26
+ items: Row[]; emptyText: string; actionMessage: string;
26
27
  isMock: boolean; notMock: boolean;
27
28
  /** #2849 — hiddenWhen() hides on TRUE, so these are inverted: the empty-state banner
28
29
  * shows only when the site really has none, and the unavailable one only when the
@@ -100,17 +101,18 @@ const COPY: LoadCopy<Row> = {
100
101
  sample: () => MOCK.map((r) => ({ ...r })),
101
102
  };
102
103
 
104
+ const defineService = withOnlineWrites(registerService, ["markOneRead","markAll"], { messageFields: ["actionMessage"] });
103
105
  const svc = defineService<S, {
104
106
  load: (s: S) => Promise<S>;
105
107
  markOneRead: (s: S, id: string) => Promise<S>;
106
108
  markAll: (s: S) => Promise<S>;
107
109
  }>("himiBrandedNotifications", {
108
- state: { ...LOADING, items: [], emptyText: "" },
110
+ state: { ...LOADING, items: [], emptyText: "", actionMessage: "" },
109
111
  actions: {
110
112
  load: async (s) => {
111
113
  try {
112
114
  const items = (await queryNotifications(25)).map(norm);
113
- return { ...s, ...loaded("items", items, COPY) };
115
+ return { ...s, ...loaded("items", items, COPY), actionMessage: "" };
114
116
  } catch (err) {
115
117
  return { ...s, ...loadFailed("items", err, COPY) };
116
118
  }
@@ -1,3 +1,4 @@
1
+ import { withOnlineWrites } from "../_shared/online-actions.js";
1
2
  // himi-branded-lite-notifsettings — reads the site's notification topics + the member's preferences.
2
3
  // a failed query is an error with Retry (or Sign in when Wix refuses the
3
4
  // visitor) and an empty one is a real empty state — never sample rows presented as
@@ -5,7 +6,7 @@
5
6
  // (_shared/branded-load-state.ts).
6
7
  // any exist. INTERACTIVE: tapping a row toggles that topic On/Off — optimistic +
7
8
  // best-effort upsertMyNotificationPreferences (a no-op on sample rows).
8
- import { defineService } from "@himalaya/state";
9
+ import { defineService as registerService } from "@himalaya/state";
9
10
  import { withEmptyFlags } from "../_shared/empty-flags.js";
10
11
  import { LOADING, loaded, loadFailed, type LoadCopy, type LoadFlags } from "../_shared/branded-load-state.js";
11
12
  import {
@@ -31,7 +32,7 @@ interface Row { id: string; name: string; subtitle: string; note: string; on: bo
31
32
  /** Inverse of `locked` (hiddenWhen hides on TRUE) and the plain On/Off a locked row shows. */
32
33
  notLocked: boolean; stateLabel: string }
33
34
  interface S extends LoadFlags {
34
- topics: Row[]; emptyText: string;
35
+ topics: Row[]; emptyText: string; actionMessage: string;
35
36
  isMock: boolean; notMock: boolean; /** #2849 — hiddenWhen() hides on TRUE, so these are inverted: the empty-state banner
36
37
  * shows only when the site really has none, and the unavailable one only when the
37
38
  * query failed. A failed query must never render the sentence that claims the
@@ -91,11 +92,12 @@ const COPY: LoadCopy<Row> = {
91
92
  sample: () => MOCK.map((r) => ({ ...r })),
92
93
  };
93
94
 
95
+ const defineService = withOnlineWrites(registerService, ["toggle"], { messageFields: ["actionMessage"] });
94
96
  const svc = defineService<S, {
95
97
  load: (s: S) => Promise<S>;
96
98
  toggle: (s: S, p?: { id?: string; value?: unknown }) => Promise<S>;
97
99
  }>("himiBrandedNotifSettings", withEmptyFlags({
98
- state: { ...LOADING, topics: [], emptyText: "" },
100
+ state: { ...LOADING, topics: [], emptyText: "", actionMessage: "" },
99
101
  actions: {
100
102
  load: async (s) => {
101
103
  try {
@@ -105,7 +107,7 @@ const svc = defineService<S, {
105
107
  ]);
106
108
  const prefs = readPreferences(prefsRes);
107
109
  const topics = readTopics(topicsRes).map((t) => norm(t, prefs));
108
- return { ...s, ...loaded("topics", topics, COPY) };
110
+ return { ...s, ...loaded("topics", topics, COPY), actionMessage: "" };
109
111
  } catch (err) {
110
112
  return { ...s, ...loadFailed("topics", err, COPY) };
111
113
  }
@@ -1,7 +1,8 @@
1
+ import { withOnlineWrites } from "../_shared/online-actions.js";
1
2
  import { createIapCommerce, classifyCart } from "@himalaya/iap-checkout";
2
3
  import { getStoreProduct } from "../_shared/himi-store.js";
3
4
  import { iapPriceCopy } from "../_shared/iap-worker.js";
4
- import { defineService } from "@himalaya/state";
5
+ import { defineService as registerService } from "@himalaya/state";
5
6
  import { orderMember, assertOrderMember, ownedOrderCache, orderAccessDenied, orderAccessChanged, type OrderMember } from "../_shared/member-orders.js";
6
7
  import { addToCurrentCart, getOrder, searchOrders, type WixOrder } from "@himalaya/wix-ecom";
7
8
  import { copyParam, copyTemplate, stashCopy } from "../_shared/himi-copy.js";
@@ -313,6 +314,7 @@ async function load(state: State, arg?: unknown): Promise<State | undefined> {
313
314
  const FOLLOW_TTL_SECONDS = 24 * 60 * 60;
314
315
 
315
316
  const reorderPricing = createIapCommerce();
317
+ const defineService = withOnlineWrites(registerService, ["reorder", "follow", "unfollow"], { derive, messageFields: action => action === "reorder" ? ["reorderMessage"] : ["liveNote"] });
316
318
  const svc = defineService<State, {
317
319
  load: (state: State, arg?: unknown) => Promise<State | undefined>;
318
320
  retry: (state: State) => Promise<State | undefined>;
@@ -1,4 +1,6 @@
1
- import { defineService } from "../_shared/collection-service.js";
1
+ import { offlineWriteMessage, offlineWritesEnabled, withOnlineWrites } from "../_shared/online-actions.js";
2
+ import { connectivity } from "@himalaya/state";
3
+ import { defineService as registerService } from "../_shared/collection-service.js";
2
4
  import { createOnlineOrder, listPublicPlans, type WixPlan } from "@himalaya/wix-pricing-plans";
3
5
  import { createRedirectSession } from "@himalaya/wix-redirects";
4
6
  import { open as openUrl } from "@himalaya/open-url";
@@ -252,6 +254,8 @@ async function regularJoinMessage(row: PlanRow, reference: { kind: string; id: s
252
254
  }
253
255
  return copyTemplate("plans.joinOnSiteMessage", "Memberships are purchased on the website — visit the site to join {name}.", { name: row.name });
254
256
  }
257
+ // A link may fail to open. Its fallback becomes a write and needs its own preflight.
258
+ if (offlineWritesEnabled && !connectivity().online) return offlineWriteMessage;
255
259
  try {
256
260
  const created = await createOnlineOrder(row.id);
257
261
  const snapshotId = created.purchaseFlowId;
@@ -291,6 +295,14 @@ async function regularJoinMessage(row: PlanRow, reference: { kind: string; id: s
291
295
  return copyTemplate("plans.joinOnSiteMessage", "Memberships are purchased on the website — visit the site to join {name}.", { name: row.name });
292
296
  }
293
297
 
298
+ const defineService = withOnlineWrites(registerService, ["join"], {
299
+ messageFields: ["actionMessage"],
300
+ requiresConnectivity: (_action, state: State, arg) => {
301
+ const row = state.rows.find(candidate => candidate.id === idOf(arg));
302
+ if (!row || commerce.purchaseFields({ kind: "pricingPlan", id: row.id }, iapPurchaseCopy("plans")).appleRoute) return true;
303
+ return !row.joinUrl && !(isHostedWorker() && !row.free);
304
+ },
305
+ });
294
306
  const svc = defineService<State, {
295
307
  load: (state: State, arg?: unknown) => Promise<State | undefined>;
296
308
  pricingPublish: (state: State) => State;
@@ -1,3 +1,4 @@
1
+ import { withOnlineWrites } from "../_shared/online-actions.js";
1
2
  // Tier-5 bundle: himi-branded-lite — single online-program detail (design §3.5
2
3
  // `programs` vertical, screen full-potential pass).
3
4
  //
@@ -19,7 +20,7 @@
19
20
  // reads every program as `disabled` and keeps Enrol on its own commerce. An Apple-routed program
20
21
  // offers Get access in place of Enrol and sells only through the shared checkout, never through the
21
22
  // Wix payment rail.
22
- import { defineService } from "@himalaya/state";
23
+ import { defineService as registerService } from "@himalaya/state";
23
24
  import {
24
25
  getProgram,
25
26
  joinProgram,
@@ -386,6 +387,7 @@ type Actions = {
386
387
  iapPoll: (state: State) => Promise<undefined>;
387
388
  };
388
389
 
390
+ const defineService = withOnlineWrites(registerService, ["enroll","buy"], { derive, messageFields: ["actionMessage"] });
389
391
  const svc = defineService<State, Actions>("himiBrandedProgram", {
390
392
  state: INITIAL_STATE,
391
393
  notHydrated: [...PRICING_NOT_HYDRATED],
@@ -1,3 +1,4 @@
1
+ import { withOnlineWrites } from "../_shared/online-actions.js";
1
2
  // himi-branded-lite-reservations — branded-lite "My Reservations" worker. Reads
2
3
  // the member's real Wix Table Reservations (queryReservations). A failed query is an
3
4
  // error with Retry (or Sign in when Wix refuses the visitor), an empty one is a real
@@ -11,7 +12,7 @@
11
12
  // flat grid it replaced repeated "Fri, Aug 8 · 7:30 PM" on every card because
12
13
  // no row knew what day the row above it was on. `Agenda` owns the day headers
13
14
  // and the time column, so a row carries only what is actually per-reservation.
14
- import { defineService } from "@himalaya/state";
15
+ import { defineService as registerService } from "@himalaya/state";
15
16
  import { LOADING, loaded, loadFailed, type LoadCopy, type LoadFlags } from "../_shared/branded-load-state.js";
16
17
  import { queryReservations, cancelReservation, type WixReservation } from "@himalaya/wix-table-reservations";
17
18
  import { type CalendarEvent } from "@himalaya/time";
@@ -21,7 +22,7 @@ interface S extends LoadFlags {
21
22
  reservations: Row[];
22
23
  /** The Agenda wire shape, derived from `reservations` on every transition. */
23
24
  events: CalendarEvent[];
24
- emptyText: string;
25
+ emptyText: string; actionMessage: string;
25
26
  isMock: boolean; notMock: boolean; /** #2849 — hiddenWhen() hides on TRUE, so these are inverted: the empty-state banner
26
27
  * shows only when the site really has none, and the unavailable one only when the
27
28
  * query failed. A failed query must never render the sentence that claims the
@@ -144,11 +145,12 @@ function tappedId(p: unknown): string {
144
145
  return "";
145
146
  }
146
147
 
148
+ const defineService = withOnlineWrites(registerService, ["cancel"], { messageFields: ["actionMessage"] });
147
149
  const svc = defineService<S, {
148
150
  load: (s: S) => Promise<S>;
149
151
  cancel: (s: S, p?: unknown) => Promise<S>;
150
152
  }>("himiBrandedReservations", {
151
- state: { ...LOADING, reservations: [], events: [], emptyText: "" },
153
+ state: { ...LOADING, reservations: [], events: [], emptyText: "", actionMessage: "" },
152
154
  actions: {
153
155
  load: async (s) => {
154
156
  try {
@@ -156,7 +158,7 @@ const svc = defineService<S, {
156
158
  // to hand it chronological order rather than newest-first.
157
159
  const r = await queryReservations({ cursorPaging: { limit: 20 }, sort: [{ fieldName: "details.startDate", order: "ASC" }] });
158
160
  const reservations = (r.reservations ?? []).map(norm);
159
- return settle(s, loaded("reservations", reservations, COPY));
161
+ return settle({ ...s, actionMessage: "" }, loaded("reservations", reservations, COPY));
160
162
  } catch (err) {
161
163
  return settle(s, loadFailed("reservations", err, COPY));
162
164
  }
@@ -1,3 +1,4 @@
1
+ import { withOnlineWrites } from "../_shared/online-actions.js";
1
2
  // Tier-5 bundle: himi-branded-lite — reservation request form (design §4.4).
2
3
  //
3
4
  // The visitor rung of Wix Table Reservations, per the REST sample flow:
@@ -19,7 +20,7 @@
19
20
  // on a Tuesday the restaurant is shut. A day strip over a bookable-times run is
20
21
  // how every real reservation flow asks the question, and both presets ride
21
22
  // FilterChips, so they render on all seven surfaces with no proto change.
22
- import { defineService } from "@himalaya/state";
23
+ import { defineService as registerService } from "@himalaya/state";
23
24
  import {
24
25
  createHeldReservation,
25
26
  listReservationLocations,
@@ -255,6 +256,7 @@ async function load(state: State, arg?: unknown): Promise<State> {
255
256
  }
256
257
  }
257
258
 
259
+ const defineService = withOnlineWrites(registerService, ["submit"], { derive });
258
260
  const svc = defineService<State, {
259
261
  load: (state: State, arg?: unknown) => Promise<State>;
260
262
  setPartySize: (state: State, arg?: unknown) => State;
@@ -1,3 +1,4 @@
1
+ import { withOnlineWrites } from "../_shared/online-actions.js";
1
2
  // Tier-5 bundle: Branded Lite — My Wallet (the member's saved payment methods).
2
3
  //
3
4
  // The cards a member chose to save at checkout, read from Wix Payments on the member's
@@ -5,7 +6,7 @@
5
6
  // page, a member can make one the default for checkout or remove one — there is no
6
7
  // "add a card" here: a card is saved while paying. Only the last four digits, the
7
8
  // expiry and the cardholder name ever reach the app.
8
- import { defineService } from "@himalaya/state";
9
+ import { defineService as registerService } from "@himalaya/state";
9
10
  import {
10
11
  deleteSavedPaymentMethod,
11
12
  listMySavedPaymentMethods,
@@ -173,6 +174,7 @@ async function write(state: State, run: () => Promise<unknown>, done: string, fa
173
174
  }
174
175
  }
175
176
 
177
+ const defineService = withOnlineWrites(registerService, ["setPrimary", "remove"], { derive });
176
178
  const svc = defineService<State, {
177
179
  load: (state: State, arg?: unknown) => Promise<State>;
178
180
  retry: (state: State) => Promise<State>;
@@ -1,6 +1,7 @@
1
1
  // Tier-5 bundle: himi-events — event detail.
2
2
  import { formatCurrency } from "../_shared/engine-intl.js";
3
- import { defineService } from "@himalaya/state";
3
+ import { defineService as registerService } from "@himalaya/state";
4
+ import { withOnlineWrites } from "../_shared/online-actions.js";
4
5
  import { createEvent, getPermissionStatus, requestPermission } from "@himalaya/calendar";
5
6
  import { open } from "@himalaya/open-url";
6
7
  import { share as shareSheet } from "@himalaya/share";
@@ -431,6 +432,12 @@ async function enrichRegistration(id: string, e: WixEvent): Promise<WixEvent> {
431
432
  }
432
433
  }
433
434
 
435
+ const defineService = withOnlineWrites(registerService, ["buyApple", "openRegistration"], {
436
+ derive,
437
+ messageFields: ["actionMessage"],
438
+ requiresConnectivity: (action, state) => action !== "openRegistration"
439
+ || (state.registrationKind === "tickets" && !!state.id && !isHostedWorker()),
440
+ });
434
441
  const svc = defineService<S, {
435
442
  load: (s: S, arg?: unknown) => Promise<S | undefined>;
436
443
  openMap: (s: S) => Promise<S>;
@@ -1,5 +1,6 @@
1
+ import { withOnlineWrites, offlineWritesEnabled } from "../_shared/online-actions.js";
1
2
  // Tier-5 bundle: himi-events — member RSVP flow.
2
- import { defineService } from "@himalaya/state";
3
+ import { defineService as registerService } from "@himalaya/state";
3
4
  import { dispatch } from "@himalaya/dispatch";
4
5
  import { createRsvp, getEvent, type WixEvent } from "@himalaya/wix-events";
5
6
  import { displayName, getMyData, type WixMyData } from "@himalaya/wix-users";
@@ -125,6 +126,7 @@ function contactName(dataLine: string): { first: string; last: string } {
125
126
  return { first: parts[0], last: parts.slice(1).join(" ") };
126
127
  }
127
128
 
129
+ const defineService = withOnlineWrites(registerService, ["rsvp", "signIn"], { derive, secretFields: ["password"] });
128
130
  const svc = defineService<S, {
129
131
  load: (s: S, arg?: unknown) => Promise<S>;
130
132
  setEmail: (s: S, arg?: unknown) => S;
@@ -202,16 +204,17 @@ const svc = defineService<S, {
202
204
  // Optimistic local write — the RSVP row lands in the cache before the Wix POST.
203
205
  const rsvpRow = (id: string): DbEntity => ({ id, eventId: s.id, firstName: name.first, lastName: name.last, email: guestEmail, status: "YES", title: s.title });
204
206
  const localId = s.id;
205
- await cachePut(rsvpRow(localId));
207
+ if (!offlineWritesEnabled) await cachePut(rsvpRow(localId));
206
208
  try {
207
209
  const res = await createRsvp(s.id, name.first, name.last, guestEmail, "YES");
208
210
  // Reconcile the optimistic local id with the server-assigned rsvp id.
209
211
  const serverId = res.rsvp?.id;
210
- if (serverId && serverId !== localId) { await cacheDrop(localId); await cachePut(rsvpRow(serverId)); }
212
+ if (offlineWritesEnabled) await cachePut(rsvpRow(serverId || localId));
213
+ else if (serverId && serverId !== localId) { await cacheDrop(localId); await cachePut(rsvpRow(serverId)); }
211
214
  rememberRsvp(s.id);
212
215
  return derive({ ...s, successMessage: copyParam("rsvp.successMessage", "You are going. Added to My Events."), message: "" });
213
216
  } catch (e) {
214
- if (isBridgeUnavailable(e)) {
217
+ if (!offlineWritesEnabled && isBridgeUnavailable(e)) {
215
218
  // Offline / no bridge: the optimistic cache row already shows; queue the
216
219
  // write to replay (WriteQueue, idempotent) when connectivity returns.
217
220
  await queueWrite("tier5.himiEventsRsvp.rsvp", { id: s.id, firstName: name.first, lastName: name.last, email: guestEmail, status: "YES" });
@@ -1,3 +1,5 @@
1
+ import { withOnlineWrites, offlineWritesEnabled, offlineWriteMessage } from "../_shared/online-actions.js";
2
+ import { defineService as registerState } from "@himalaya/state";
1
3
  // Tier-5 bundle: himi-store — product detail.
2
4
  import { withIapPresentation } from "../_shared/iap-presentation.js";
3
5
  import { storesAppleSalesEnabled, storesProductOrders, type IapCommerce, type IapPurchaseFields } from "@himalaya/iap-checkout";
@@ -459,6 +461,7 @@ const NOT_APPLE: IapPurchaseFields = { appleRoute: false, accessReady: false, bu
459
461
  let productPricing: IapCommerce;
460
462
  let checkoutEntered = false;
461
463
  const defineService = withIapPresentation<S>({
464
+ register: withOnlineWrites(registerState, ["beginAdd", "addToCart", "buy"], { derive, messageFields: action => action === "buy" ? ["buyNote"] : ["cartHint"] }),
462
465
  adapters: SELLS_THROUGH_APPLE ? [storesProductOrders] : [],
463
466
  onCreate: commerce => { productPricing = commerce; }, prefix: "product",
464
467
  transientKeys: ["price", "optionsIncomplete", "cartHint", "cartHidden", "buyHidden", "buyLabel", "buyDisabled", "buyNote", "purchasedNote", "addedSheetPresented", "shareHint"], contentActions: ["load", "paintCached"], concurrentPublishActions: ["buy"],
@@ -467,12 +470,14 @@ const defineService = withIapPresentation<S>({
467
470
  const purchase = applePurchase(state);
468
471
  const unavailable = iapPriceCopy("product").purchaseUnavailable;
469
472
  const blocked = lastProduct?.productType?.toLowerCase() === "digital" || (!state.optionsUnknown && !allowed(inputId()));
470
- const cartHint = !purchase.appleRoute && blocked ? unavailable : state.cartHint === unavailable ? "" : state.cartHint;
473
+ const cartHint = state.cartHint === offlineWriteMessage ? state.cartHint
474
+ : !purchase.appleRoute && blocked ? unavailable : state.cartHint === unavailable ? "" : state.cartHint;
471
475
  return {
472
476
  ...state, price: price(inputId()), ...(purchase.appleRoute ? { wasPrice: "", wasPriceEmpty: false, notOnSale: true } : {}), optionsIncomplete: derive(state).optionsIncomplete || !allowed(inputId()) || lastProduct?.productType?.toLowerCase() === "digital",
473
477
  cartHint, noCartHint: !cartHint || state.added, addDisabled: state.addDisabled || blocked,
474
478
  cartHidden: purchase.appleRoute,
475
- buyHidden: !purchase.appleRoute || purchase.accessReady, buyLabel: purchase.buyLabel, buyDisabled: purchase.buyDisabled, buyNote: purchase.buyNote,
479
+ buyHidden: !purchase.appleRoute || purchase.accessReady, buyLabel: purchase.buyLabel, buyDisabled: purchase.buyDisabled,
480
+ buyNote: state.buyNote === offlineWriteMessage ? state.buyNote : purchase.buyNote,
476
481
  purchasedNote: purchase.accessReady ? copyParam("product.purchasedText", "Purchased") : "",
477
482
  };
478
483
  },
@@ -848,11 +853,11 @@ const svc = defineService<S, {
848
853
  const sale = lastProduct ? productSale(lastProduct) : { wasPrice: "" };
849
854
  const swatches = s.optionGroups.flatMap(g => g.kind === "color" ? g.choices.map(c => c.colorCode).filter(Boolean) : []).slice(0, 5);
850
855
  const line: DbEntity = { id: variantKey ? `line-${id}-${variantKey}` : `line-${id}`, productId: id, name: s.name || "product", title: s.name || "product", subtitle: chosen ? `${chosen} · Qty ${quantity}` : `Qty ${quantity}`, quantity: String(quantity), image: s.image, unitPrice, wasPrice: sale.wasPrice, swatches, total: optimisticLineTotal(unitPrice, quantity), hasImage: !!s.image, noImage: !s.image };
851
- await cartPut(line);
856
+ if (!offlineWritesEnabled) await cartPut(line);
852
857
  try {
853
858
  await addToCurrentCart(id, quantity, cartOptions);
854
859
  } catch (e) {
855
- if (isBridgeUnavailable(e)) {
860
+ if (!offlineWritesEnabled && isBridgeUnavailable(e)) {
856
861
  // Offline / no bridge: the optimistic cache line already shows; queue the
857
862
  // add to replay (WriteQueue, idempotent) when connectivity returns.
858
863
  await queueWrite("tier5.himiStoreProduct.addToCart", { id, ...(cartOptions ? { options: JSON.stringify(cartOptions) } : {}) });
@@ -861,6 +866,7 @@ const svc = defineService<S, {
861
866
  // Roll the optimistic badge back — nothing was added.
862
867
  return derive({ ...s, adding: false, added: false, cartCount: before, cartHint: copyParam("product.cartHintFailed", "Could not add to cart. Try again.") });
863
868
  }
869
+ if (offlineWritesEnabled) await cartPut(line);
864
870
  // The write LANDED. The badge re-read is outside the write's try on purpose: a failed
865
871
  // count must never be mistaken for a failed add. Server count wins; else keep optimistic.
866
872
  const count = await cartItemCount().catch(() => 0);
@@ -1,3 +1,4 @@
1
+ import { withOnlineWrites } from './online-actions.js';
1
2
  import { defineService as register, type ServiceDefinition } from '@himalaya/state';
2
3
  import { COLLECTIONS, COLLECTION_CHROME_MIN_ITEMS, initialCollection, normalizeCollection, projectCollection, collectionId, collectionActionsLabel, type CollectionRow } from './collection-contract.js';
3
4
  import { localPreferences, type PreferenceRuntime } from './local-preferences.js';
@@ -331,6 +332,8 @@ export function decorateCollection(name: string, def: { state: State; actions?:
331
332
  * reducer and reads the private controller snapshot, never an SDUI payload. */
332
333
  export function defineBackendCollection<S extends object, Row extends object>(name: string, options: {
333
334
  register?: typeof register;
335
+ /** Explicit mutation preflight; unrelated collection services keep their current reducers. */
336
+ onlineWrites?: boolean;
334
337
  /** Revalidate already opened pages on navigation return; explicit refresh resets. */
335
338
  restorePagesOnLoad?: boolean;
336
339
  /** The load action's argument (onAppear params beyond `copy`), before the query runs — for a
@@ -350,7 +353,8 @@ export function defineBackendCollection<S extends object, Row extends object>(na
350
353
  controller: controller as unknown as BackendCollection['controller'],
351
354
  apply: (state, page) => options.apply(state as S, page as unknown as CollectionQueryState<Row>) as State,
352
355
  });
353
- const handle = (options.register ?? register)(name, definition);
356
+ const registerService = options.onlineWrites ? withOnlineWrites(options.register ?? register, ['confirmCancel', 'confirmOperation', 'retryOperation', 'commitWrite'], { messageFields: ['actionMessage'] }) : (options.register ?? register);
357
+ const handle = registerService(name, definition);
354
358
  // Registration restored the state a cold boot saved: the page it showed is this run's, before the first load. Only
355
359
  // when the service's own rows came back with it: a bundle that keeps them out of the restore (its `notHydrated`)
356
360
  // starts its first load as before, never from rows it chose not to restore. Rows that came back empty are the
@@ -0,0 +1,76 @@
1
+ import { connectivity, defineService as register, type ServiceDefinition } from '@himalaya/state';
2
+
3
+ // Template generation enables this behavior only in the selected two starter families.
4
+ export const offlineWritesEnabled = false;
5
+
6
+ export const offlineWriteMessage = 'You’re offline. Connect to save this change, then submit it again.';
7
+
8
+ /** The guard runs before the reducer: entered values survive and no optimistic
9
+ * changes, success state, or queued writes can occur. It never submits on reconnect. */
10
+ export function offlineWriteState<S extends object>(state: S): S {
11
+ const next: Record<string, unknown> = { ...state, error: offlineWriteMessage, message: offlineWriteMessage };
12
+ for (const key of ['saving', 'isSaving', 'submitting', 'isSubmitting', 'adding', 'added', 'bookingInFlight']) {
13
+ if (key in next) next[key] = false;
14
+ }
15
+ for (const key of ['savedMessage', 'successMessage']) {
16
+ if (key in next) next[key] = '';
17
+ }
18
+ // A saved/favourited item is business state, not a submission-success flag.
19
+ return next as S;
20
+ }
21
+
22
+ interface OfflineWriteOptions<S> {
23
+ derive?: (state: S) => S;
24
+ /** Exempt only non-mutating branches of a registered action. */
25
+ requiresConnectivity?: (action: string, state: S, argument?: unknown) => boolean;
26
+ messageFields?: readonly string[] | ((action: string) => readonly string[]);
27
+ draftFields?: readonly string[] | Readonly<Record<string, string>>;
28
+ secretFields?: readonly string[];
29
+ }
30
+
31
+ export function withOnlineWrites<S extends object>(registerService: typeof register, names: readonly string[], options: OfflineWriteOptions<S> = {}): typeof register {
32
+ if (!offlineWritesEnabled) return registerService;
33
+ return ((name: string, definition: ServiceDefinition<Record<string, unknown>>) => {
34
+ let currentState: (() => Record<string, unknown>) | undefined;
35
+ const handle = registerService(name, {
36
+ ...definition,
37
+ notHydrated: [...new Set([...(definition.notHydrated ?? []), ...(options.secretFields ?? [])])],
38
+ actions: Object.fromEntries(Object.entries(definition.actions ?? {}).map(([action, reduce]) => [action,
39
+ (state: Record<string, unknown>, ...args: never[]) => {
40
+ if (!names.includes(action)) return reduce(state, ...args);
41
+ if (options.requiresConnectivity?.(action, state as S, args[0]) === false) return reduce(state, ...args);
42
+ const fields = typeof options.messageFields === 'function' ? options.messageFields(action) : options.messageFields ?? ['message'];
43
+ if (connectivity().online) {
44
+ let cleared = false;
45
+ for (const field of ['error', 'message', ...fields]) {
46
+ if (state[field] === offlineWriteMessage) { state[field] = ''; cleared = true; }
47
+ }
48
+ // Keep the runtime's state object: reducers may mutate it and return undefined.
49
+ const result = reduce(state, ...args);
50
+ if (!cleared) return result;
51
+ // Publish cleared feedback even for a void reducer. An async completion must
52
+ // keep any newer state published by another action while it was awaiting.
53
+ const finish = (next: Record<string, unknown> | undefined) => next === undefined ? currentState?.() ?? state : next;
54
+ return result && typeof (result as Promise<unknown>).then === 'function'
55
+ ? Promise.resolve(result).then(finish)
56
+ : finish(result as Record<string, unknown> | undefined);
57
+ }
58
+ const draft = { ...state };
59
+ const argument = args[0] as Record<string, unknown> | undefined;
60
+ const draftFields = options.draftFields ?? [];
61
+ const entries = Array.isArray(draftFields) ? draftFields.map(key => [key, key]) : Object.entries(draftFields);
62
+ for (const [key, source] of entries) {
63
+ if (argument && key in state && typeof argument[source] === typeof state[key]) draft[key] = argument[source];
64
+ }
65
+ for (const key of options.secretFields ?? []) draft[key] = '';
66
+ const feedback = Object.fromEntries(fields.map(key => [key, offlineWriteMessage]));
67
+ let next = { ...offlineWriteState(draft), ...feedback };
68
+ if (options.derive) next = options.derive(next as S) as Record<string, unknown>;
69
+ return { ...next, ...feedback };
70
+ },
71
+ ])),
72
+ });
73
+ if (typeof handle.getState === 'function') currentState = () => handle.getState();
74
+ return handle;
75
+ }) as typeof register;
76
+ }
@@ -1,3 +1,4 @@
1
+ import { withOnlineWrites } from "../_shared/online-actions.js";
1
2
  // Tier-5 bundle: himi-branded-lite — community activity feed (design §4.5).
2
3
  //
3
4
  // The feed rides the Wix Groups social feed: a feed item lives in a group
@@ -9,7 +10,7 @@
9
10
  // worker's signed-in flags (the himi_group join-button pattern) and the
10
11
  // sign-in card routes to himi_account — never a dead tap.
11
12
  // Four-state (loading / error / rows / empty) like the sibling branded workers.
12
- import { defineService } from "@himalaya/state";
13
+ import { defineService as registerService } from "@himalaya/state";
13
14
  import {
14
15
  createFeedItem,
15
16
  listFeedItems,
@@ -301,6 +302,7 @@ async function loadFeed(state: State): Promise<State> {
301
302
  }
302
303
  }
303
304
 
305
+ const defineService = withOnlineWrites(registerService, ["post", "react"], { derive, draftFields: { draft: "message" } });
304
306
  const svc = defineService<State, {
305
307
  load: (state: State, arg?: unknown) => Promise<State>;
306
308
  filter: (state: State, arg?: unknown) => State;
@@ -1,4 +1,5 @@
1
- import { defineService } from "@himalaya/state";
1
+ import { withOnlineWrites } from "../_shared/online-actions.js";
2
+ import { defineService as registerService } from "@himalaya/state";
2
3
  import { getGroup, joinGroup, leaveGroup, queryMembers as queryGroupMembers, type WixGroup } from "@himalaya/wix-groups";
3
4
  import { getMember } from "@himalaya/wix-members";
4
5
  import { signedInEmail } from "../_shared/himi-account";
@@ -159,6 +160,7 @@ const initial: State = {
159
160
  hideContent: true,
160
161
  };
161
162
 
163
+ const defineService = withOnlineWrites(registerService, ["join","leave"]);
162
164
  const svc = defineService<State, {
163
165
  load: (state: State, arg?: unknown) => Promise<State>;
164
166
  join: (state: State) => Promise<State>;
@@ -1,6 +1,7 @@
1
1
  // Tier-5 bundle: himi-events — event detail.
2
2
  import { formatCurrency } from "../_shared/engine-intl.js";
3
- import { defineService } from "@himalaya/state";
3
+ import { defineService as registerService } from "@himalaya/state";
4
+ import { withOnlineWrites } from "../_shared/online-actions.js";
4
5
  import { createEvent, getPermissionStatus, requestPermission } from "@himalaya/calendar";
5
6
  import { open } from "@himalaya/open-url";
6
7
  import { share as shareSheet } from "@himalaya/share";
@@ -431,6 +432,12 @@ async function enrichRegistration(id: string, e: WixEvent): Promise<WixEvent> {
431
432
  }
432
433
  }
433
434
 
435
+ const defineService = withOnlineWrites(registerService, ["buyApple", "openRegistration"], {
436
+ derive,
437
+ messageFields: ["actionMessage"],
438
+ requiresConnectivity: (action, state) => action !== "openRegistration"
439
+ || (state.registrationKind === "tickets" && !!state.id && !isHostedWorker()),
440
+ });
434
441
  const svc = defineService<S, {
435
442
  load: (s: S, arg?: unknown) => Promise<S | undefined>;
436
443
  openMap: (s: S) => Promise<S>;