@wix/himalaya-cli 0.1525.0 → 0.1527.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 (177) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/builder.d.ts +14 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/state/src/connectivity.d.ts +5 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/state/src/index.d.ts +1 -0
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +22 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/worker-subtree-safe-types.generated.d.ts +4 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +7 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +13 -0
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/state.mjs +14 -0
  9. package/dist/cli.mjs +157 -40
  10. package/dist/preview-assets.generated.bin +0 -0
  11. package/guides/designing-himi-screens.md +43 -18
  12. package/guides/himi-test.md +3 -3
  13. package/guides/recipe-dynamic-list.md +7 -7
  14. package/guides/recipe-four-state.md +47 -41
  15. package/guides/recipe-search.md +1 -1
  16. package/package.json +2 -2
  17. package/templates/beauty-salon/dev/index.ts +1943 -646
  18. package/templates/beauty-salon/tier5-src/_shared/collection-service.ts +5 -1
  19. package/templates/beauty-salon/tier5-src/_shared/online-actions.ts +76 -0
  20. package/templates/beauty-salon/tier5-src/himi-booking-bookings/index.ts +1 -0
  21. package/templates/beauty-salon/tier5-src/himi-booking-time/index.ts +803 -284
  22. package/templates/beauty-salon/tier5-src/himi-branded-lite-address/index.ts +3 -1
  23. package/templates/beauty-salon/tier5-src/himi-branded-lite-addresses/index.ts +3 -1
  24. package/templates/beauty-salon/tier5-src/himi-branded-lite-agenda/index.ts +9 -1
  25. package/templates/beauty-salon/tier5-src/himi-branded-lite-cart/index.ts +3 -0
  26. package/templates/beauty-salon/tier5-src/himi-branded-lite-order/index.ts +3 -1
  27. package/templates/beauty-salon/tier5-src/himi-branded-lite-place-summary/index.ts +12 -1
  28. package/templates/beauty-salon/tier5-src/himi-branded-lite-plans/index.ts +13 -1
  29. package/templates/beauty-salon/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
  30. package/templates/beauty-salon/tier5-src/himi-store-account/index.ts +69 -10
  31. package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +10 -4
  32. package/templates/beauty-salon-owner/tier5-src/_shared/collection-service.ts +5 -1
  33. package/templates/beauty-salon-owner/tier5-src/_shared/online-actions.ts +76 -0
  34. package/templates/branded-lite/dev/index.ts +2404 -1020
  35. package/templates/branded-lite/tier5-src/_shared/collection-service.ts +5 -1
  36. package/templates/branded-lite/tier5-src/_shared/online-actions.ts +76 -0
  37. package/templates/branded-lite/tier5-src/business-chat/index.ts +4 -0
  38. package/templates/branded-lite/tier5-src/himi-booking-bookings/index.ts +1 -0
  39. package/templates/branded-lite/tier5-src/himi-booking-time/index.ts +803 -284
  40. package/templates/branded-lite/tier5-src/himi-branded-lite-address/index.ts +3 -1
  41. package/templates/branded-lite/tier5-src/himi-branded-lite-addresses/index.ts +3 -1
  42. package/templates/branded-lite/tier5-src/himi-branded-lite-agenda/index.ts +9 -1
  43. package/templates/branded-lite/tier5-src/himi-branded-lite-cart/index.ts +3 -0
  44. package/templates/branded-lite/tier5-src/himi-branded-lite-feed/index.ts +3 -1
  45. package/templates/branded-lite/tier5-src/himi-branded-lite-group/index.ts +3 -1
  46. package/templates/branded-lite/tier5-src/himi-branded-lite-notifications/index.ts +6 -4
  47. package/templates/branded-lite/tier5-src/himi-branded-lite-notifsettings/index.ts +6 -4
  48. package/templates/branded-lite/tier5-src/himi-branded-lite-order/index.ts +3 -1
  49. package/templates/branded-lite/tier5-src/himi-branded-lite-place-summary/index.ts +12 -1
  50. package/templates/branded-lite/tier5-src/himi-branded-lite-plans/index.ts +13 -1
  51. package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +3 -1
  52. package/templates/branded-lite/tier5-src/himi-branded-lite-reservations/index.ts +6 -4
  53. package/templates/branded-lite/tier5-src/himi-branded-lite-reserve/index.ts +3 -1
  54. package/templates/branded-lite/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
  55. package/templates/branded-lite/tier5-src/himi-events-detail/index.ts +8 -1
  56. package/templates/branded-lite/tier5-src/himi-events-rsvp/index.ts +7 -4
  57. package/templates/branded-lite/tier5-src/himi-store-account/index.ts +69 -10
  58. package/templates/branded-lite/tier5-src/himi-store-product/index.ts +10 -4
  59. package/templates/community-club/dev/index.ts +40 -21
  60. package/templates/community-club/tier5-src/_shared/collection-service.ts +5 -1
  61. package/templates/community-club/tier5-src/_shared/online-actions.ts +76 -0
  62. package/templates/community-club/tier5-src/himi-branded-lite-feed/index.ts +3 -1
  63. package/templates/community-club/tier5-src/himi-branded-lite-group/index.ts +3 -1
  64. package/templates/community-club/tier5-src/himi-events-detail/index.ts +8 -1
  65. package/templates/community-club/tier5-src/himi-events-rsvp/index.ts +7 -4
  66. package/templates/community-club/tier5-src/himi-store-account/index.ts +69 -10
  67. package/templates/community-club-owner/tier5-src/_shared/collection-service.ts +5 -1
  68. package/templates/community-club-owner/tier5-src/_shared/online-actions.ts +76 -0
  69. package/templates/events-meetups/dev/index.ts +36 -19
  70. package/templates/events-meetups/tier5-src/_shared/collection-service.ts +5 -1
  71. package/templates/events-meetups/tier5-src/_shared/online-actions.ts +76 -0
  72. package/templates/events-meetups/tier5-src/himi-branded-lite-feed/index.ts +3 -1
  73. package/templates/events-meetups/tier5-src/himi-branded-lite-group/index.ts +3 -1
  74. package/templates/events-meetups/tier5-src/himi-events-detail/index.ts +8 -1
  75. package/templates/events-meetups/tier5-src/himi-events-rsvp/index.ts +7 -4
  76. package/templates/events-meetups/tier5-src/himi-store-account/index.ts +69 -10
  77. package/templates/events-meetups-owner/tier5-src/_shared/collection-service.ts +5 -1
  78. package/templates/events-meetups-owner/tier5-src/_shared/online-actions.ts +76 -0
  79. package/templates/gym-fitness/dev/index.ts +1944 -645
  80. package/templates/gym-fitness/tier5-src/_shared/collection-service.ts +5 -1
  81. package/templates/gym-fitness/tier5-src/_shared/online-actions.ts +76 -0
  82. package/templates/gym-fitness/tier5-src/himi-booking-bookings/index.ts +1 -0
  83. package/templates/gym-fitness/tier5-src/himi-booking-time/index.ts +803 -284
  84. package/templates/gym-fitness/tier5-src/himi-branded-lite-address/index.ts +3 -1
  85. package/templates/gym-fitness/tier5-src/himi-branded-lite-addresses/index.ts +3 -1
  86. package/templates/gym-fitness/tier5-src/himi-branded-lite-agenda/index.ts +9 -1
  87. package/templates/gym-fitness/tier5-src/himi-branded-lite-cart/index.ts +3 -0
  88. package/templates/gym-fitness/tier5-src/himi-branded-lite-order/index.ts +3 -1
  89. package/templates/gym-fitness/tier5-src/himi-branded-lite-place-summary/index.ts +12 -1
  90. package/templates/gym-fitness/tier5-src/himi-branded-lite-plans/index.ts +13 -1
  91. package/templates/gym-fitness/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
  92. package/templates/gym-fitness/tier5-src/himi-events-detail/index.ts +8 -1
  93. package/templates/gym-fitness/tier5-src/himi-events-rsvp/index.ts +7 -4
  94. package/templates/gym-fitness/tier5-src/himi-store-account/index.ts +69 -10
  95. package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +10 -4
  96. package/templates/gym-fitness-owner/tier5-src/_shared/collection-service.ts +5 -1
  97. package/templates/gym-fitness-owner/tier5-src/_shared/online-actions.ts +76 -0
  98. package/templates/nonprofit-cause/dev/index.ts +47 -29
  99. package/templates/nonprofit-cause/tier5-src/_shared/collection-service.ts +5 -1
  100. package/templates/nonprofit-cause/tier5-src/_shared/online-actions.ts +76 -0
  101. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-feed/index.ts +3 -1
  102. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-group/index.ts +3 -1
  103. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-place-summary/index.ts +12 -1
  104. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-plans/index.ts +13 -1
  105. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
  106. package/templates/nonprofit-cause/tier5-src/himi-events-detail/index.ts +8 -1
  107. package/templates/nonprofit-cause/tier5-src/himi-events-rsvp/index.ts +7 -4
  108. package/templates/nonprofit-cause/tier5-src/himi-store-account/index.ts +69 -10
  109. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-service.ts +5 -1
  110. package/templates/nonprofit-cause-owner/tier5-src/_shared/online-actions.ts +76 -0
  111. package/templates/owner-lite/tier5-src/_shared/collection-service.ts +5 -1
  112. package/templates/owner-lite/tier5-src/_shared/online-actions.ts +76 -0
  113. package/templates/podcast-creator/dev/index.ts +47 -29
  114. package/templates/podcast-creator/tier5-src/_shared/collection-service.ts +5 -1
  115. package/templates/podcast-creator/tier5-src/_shared/online-actions.ts +76 -0
  116. package/templates/podcast-creator/tier5-src/himi-branded-lite-feed/index.ts +3 -1
  117. package/templates/podcast-creator/tier5-src/himi-branded-lite-place-summary/index.ts +12 -1
  118. package/templates/podcast-creator/tier5-src/himi-branded-lite-plans/index.ts +13 -1
  119. package/templates/podcast-creator/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
  120. package/templates/podcast-creator/tier5-src/himi-events-detail/index.ts +8 -1
  121. package/templates/podcast-creator/tier5-src/himi-events-rsvp/index.ts +7 -4
  122. package/templates/podcast-creator/tier5-src/himi-store-account/index.ts +69 -10
  123. package/templates/podcast-creator-owner/tier5-src/_shared/collection-service.ts +5 -1
  124. package/templates/podcast-creator-owner/tier5-src/_shared/online-actions.ts +76 -0
  125. package/templates/portfolio-creative/dev/index.ts +52 -27
  126. package/templates/portfolio-creative/tier5-src/_shared/collection-service.ts +5 -1
  127. package/templates/portfolio-creative/tier5-src/_shared/online-actions.ts +76 -0
  128. package/templates/portfolio-creative/tier5-src/himi-branded-lite-address/index.ts +3 -1
  129. package/templates/portfolio-creative/tier5-src/himi-branded-lite-addresses/index.ts +3 -1
  130. package/templates/portfolio-creative/tier5-src/himi-branded-lite-cart/index.ts +3 -0
  131. package/templates/portfolio-creative/tier5-src/himi-branded-lite-order/index.ts +3 -1
  132. package/templates/portfolio-creative/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
  133. package/templates/portfolio-creative/tier5-src/himi-events-detail/index.ts +8 -1
  134. package/templates/portfolio-creative/tier5-src/himi-events-rsvp/index.ts +7 -4
  135. package/templates/portfolio-creative/tier5-src/himi-store-account/index.ts +69 -10
  136. package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +10 -4
  137. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-service.ts +5 -1
  138. package/templates/portfolio-creative-owner/tier5-src/_shared/online-actions.ts +76 -0
  139. package/templates/restaurant/dev/index.ts +48 -25
  140. package/templates/restaurant/tier5-src/_shared/collection-service.ts +5 -1
  141. package/templates/restaurant/tier5-src/_shared/online-actions.ts +76 -0
  142. package/templates/restaurant/tier5-src/himi-branded-lite-address/index.ts +3 -1
  143. package/templates/restaurant/tier5-src/himi-branded-lite-addresses/index.ts +3 -1
  144. package/templates/restaurant/tier5-src/himi-branded-lite-reserve/index.ts +3 -1
  145. package/templates/restaurant/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
  146. package/templates/restaurant/tier5-src/himi-events-detail/index.ts +8 -1
  147. package/templates/restaurant/tier5-src/himi-events-rsvp/index.ts +7 -4
  148. package/templates/restaurant/tier5-src/himi-store-account/index.ts +69 -10
  149. package/templates/restaurant-owner/tier5-src/_shared/collection-service.ts +5 -1
  150. package/templates/restaurant-owner/tier5-src/_shared/online-actions.ts +76 -0
  151. package/templates/retail-store/dev/index.ts +44 -23
  152. package/templates/retail-store/tier5-src/_shared/collection-service.ts +5 -1
  153. package/templates/retail-store/tier5-src/_shared/online-actions.ts +76 -0
  154. package/templates/retail-store/tier5-src/himi-branded-lite-address/index.ts +3 -1
  155. package/templates/retail-store/tier5-src/himi-branded-lite-addresses/index.ts +3 -1
  156. package/templates/retail-store/tier5-src/himi-branded-lite-cart/index.ts +3 -0
  157. package/templates/retail-store/tier5-src/himi-branded-lite-order/index.ts +3 -1
  158. package/templates/retail-store/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
  159. package/templates/retail-store/tier5-src/himi-store-account/index.ts +69 -10
  160. package/templates/retail-store/tier5-src/himi-store-product/index.ts +10 -4
  161. package/templates/retail-store-owner/tier5-src/_shared/collection-service.ts +5 -1
  162. package/templates/retail-store-owner/tier5-src/_shared/online-actions.ts +76 -0
  163. package/templates/yoga-studio/dev/index.ts +2102 -811
  164. package/templates/yoga-studio/tier5-src/_shared/collection-service.ts +5 -1
  165. package/templates/yoga-studio/tier5-src/_shared/online-actions.ts +76 -0
  166. package/templates/yoga-studio/tier5-src/himi-booking-bookings/index.ts +1 -0
  167. package/templates/yoga-studio/tier5-src/himi-booking-time/index.ts +803 -284
  168. package/templates/yoga-studio/tier5-src/himi-branded-lite-agenda/index.ts +9 -1
  169. package/templates/yoga-studio/tier5-src/himi-branded-lite-place-summary/index.ts +12 -1
  170. package/templates/yoga-studio/tier5-src/himi-branded-lite-plans/index.ts +13 -1
  171. package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +3 -1
  172. package/templates/yoga-studio/tier5-src/himi-branded-lite-wallet/index.ts +3 -1
  173. package/templates/yoga-studio/tier5-src/himi-events-detail/index.ts +8 -1
  174. package/templates/yoga-studio/tier5-src/himi-events-rsvp/index.ts +7 -4
  175. package/templates/yoga-studio/tier5-src/himi-store-account/index.ts +69 -10
  176. package/templates/yoga-studio-owner/tier5-src/_shared/collection-service.ts +5 -1
  177. package/templates/yoga-studio-owner/tier5-src/_shared/online-actions.ts +76 -0
@@ -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,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" });
@@ -26,6 +26,10 @@ interface AuthRouteInput {
26
26
  returnScreenId?: string;
27
27
  returnId?: string;
28
28
  returnChannelId?: string;
29
+ /** Standalone sign-in only: "1" when it was opened as a modal (sheet / cover). */
30
+ modal?: string;
31
+ /** Modal only: the tab whose stack the return screen opens on once the modal closes. */
32
+ returnTab?: string;
29
33
  }
30
34
 
31
35
  // The screen's boot input survives refresh; consuming it must survive too.
@@ -61,6 +65,12 @@ function authIntentFromArg(arg: unknown, prev: Pick<S, "authIntent" | "authConte
61
65
  };
62
66
  }
63
67
 
68
+ /** How the standalone form leaves. A modal is closed: on web `back` pops the tab's stack,
69
+ * not the modal. A pushed form goes back. */
70
+ function leaveNav(): "dismiss" | "back" {
71
+ return routeInput().modal === "1" ? "dismiss" : "back";
72
+ }
73
+
64
74
  /** Where a successful sign-in returns to, or null. Never Account itself — that would
65
75
  * only push a second copy of the screen the member is already on. */
66
76
  function resumeTarget(s: Pick<S, "returnScreenId" | "returnId" | "returnChannelId">): { screenId: string; args?: Record<string, string> } | null {
@@ -78,11 +88,26 @@ function resumeTarget(s: Pick<S, "returnScreenId" | "returnId" | "returnChannelI
78
88
  * On the standalone sign-in screen the target is pushed, not a `replace`: on iOS and
79
89
  * Android `replace` resets the stack to its root, which would drop the screens the
80
90
  * visitor came through. Back from the target shows the form again, and the form sends a
81
- * member back on appear (settle). With no target the form goes back. */
91
+ * member back on appear (settle). With no target the form goes back.
92
+ *
93
+ * A modal form closes. A target pushed from here would open inside the modal (iOS, Android)
94
+ * or under it (web), so a modal that names a `returnTab` hands the target to that tab: the
95
+ * host closes the modal, then pushes it on that tab's stack (Account's member rows). A host
96
+ * showing no such tab (a drawer, a narrow Mac window) drops that request whole, modal and
97
+ * all, so the modal is also dismissed directly: it always closes, and the row opens where
98
+ * the tab exists. With no `returnTab` it only closes, and the screen under it loads again —
99
+ * the screen that opened the modal is the return target. */
82
100
  function resumeAfterAuth(next: S): S {
83
101
  const target = resumeTarget(next);
84
102
  if (!next.isMember) return next;
85
- if (!target) return next.standalone ? navigate(next, { presentation: "back" }) : next;
103
+ const tab = routeInput().modal === "1" ? routeInput().returnTab ?? "" : "";
104
+ if (target && tab && next.standalone) {
105
+ routeInputConsumed = true;
106
+ void dispatch("himi.open", { presentation: "dismiss" });
107
+ const cleared = derive({ ...next, authIntent: "", authContextName: "", returnScreenId: "", returnId: "", returnChannelId: "" });
108
+ return navigate(cleared, target.args ? { tab, screenId: target.screenId, args: target.args } : { tab, screenId: target.screenId });
109
+ }
110
+ if (!target || routeInput().modal === "1") return next.standalone ? navigate(next, { presentation: leaveNav() }) : next;
86
111
  routeInputConsumed = true;
87
112
  const cleared = derive({ ...next, authIntent: "", authContextName: "", returnScreenId: "", returnId: "", returnChannelId: "" });
88
113
  return navigate(cleared, target);
@@ -96,11 +121,31 @@ function forwardToSignIn(next: S): S {
96
121
  if (!next.authIntent && !next.returnScreenId) return next;
97
122
  routeInputConsumed = true;
98
123
  const args: Record<string, string> = { mode: "signin" };
99
- for (const key of ["authIntent", "authContextName", "returnScreenId", "returnId", "returnChannelId"] as const) {
100
- if (next[key]) args[key === "authIntent" ? "intent" : key === "authContextName" ? "contextName" : key] = next[key];
124
+ for (const key of ["authIntent", "authContextName"] as const) {
125
+ if (next[key]) args[key === "authIntent" ? "intent" : "contextName"] = next[key];
126
+ }
127
+ return openSignIn(derive({ ...next, authIntent: "", authContextName: "", returnScreenId: "", returnId: "", returnChannelId: "" }),
128
+ args, resumeTarget(next));
129
+ }
130
+
131
+ /** Account sends a guest to the standalone sign-in screen, with `after`, the member screen to
132
+ * open once signed in. With a `signInPresentation` appear param it opens as that modal, and
133
+ * `after` goes on Account's own tab (`signInReturnTab`) when the modal closes; pushed, the form
134
+ * pushes `after` itself (resumeAfterAuth). */
135
+ function openSignIn(next: S, args: Record<string, string>, after: { screenId: string; args?: Record<string, string> } | null): S {
136
+ const screenId = next.signInScreen || "himi_sign_in";
137
+ const presentation = signInPresentationRequested;
138
+ const back: Record<string, string> = {};
139
+ if (after) {
140
+ back.returnScreenId = after.screenId;
141
+ if (after.args?.id) back.returnId = after.args.id;
142
+ if (after.args?.channelId) back.returnChannelId = after.args.channelId;
101
143
  }
102
- return navigate(derive({ ...next, authIntent: "", authContextName: "", returnScreenId: "", returnId: "", returnChannelId: "" }),
103
- { screenId: next.signInScreen, args });
144
+ if (!presentation) return navigate(next, { screenId, args: { ...args, ...back } });
145
+ const modalArgs: Record<string, string> = { ...args, modal: "1" };
146
+ // With no tab to open it on, a modal has no way to the member screen: it only closes.
147
+ if (after && signInReturnTabRequested) Object.assign(modalArgs, back, { returnTab: signInReturnTabRequested });
148
+ return navigate(next, { screenId, presentation, args: modalArgs });
104
149
  }
105
150
 
106
151
  /** After a load: the standalone sign-in screen sends a member back (a stale link, or a
@@ -128,8 +173,11 @@ function settle(next: S, arg: unknown, latest = true): S {
128
173
  // The appear refresh returns `_nav`, which every host applies — binaries from before
129
174
  // worker nav dispatch included. Only an arg-less load, whose result iOS
130
175
  // discards, dispatches.
131
- if (strField(arg, "standalone") === "1") return navigate(cleared, { presentation: "back" });
132
- void dispatch("himi.open", { presentation: "back" });
176
+ if (strField(arg, "standalone") === "1") return navigate(cleared, { presentation: leaveNav() });
177
+ // Not from a modal: nothing pushed over it comes back to it, and on iOS the closing modal's
178
+ // own screen reloads here once, after it has asked to close — a late dismiss would close
179
+ // whatever modal is open by then.
180
+ if (routeInput().modal !== "1") void dispatch("himi.open", { presentation: leaveNav() });
133
181
  return cleared;
134
182
  }
135
183
  // Account forwards only on an appear refresh: the boot `load()` has no params and its
@@ -180,6 +228,10 @@ let appliedSeq = 0;
180
228
  // The appear params belong to the worker session too, like the two flags above: a probe
181
229
  // dropped because the visitor typed must not take them with it.
182
230
  let signInScreenRequested = "";
231
+ /** Account only: how it opens the standalone sign-in screen ("" = push). */
232
+ let signInPresentationRequested: "" | "sheet" | "full_screen" = "";
233
+ /** Account only: its tab, where a member screen opens after a modal sign-in. */
234
+ let signInReturnTabRequested = "";
183
235
  let standaloneRequested = false;
184
236
  let pollIdentityRequested = false;
185
237
  // Bumped by every mode switch, so a dropped probe never puts back a mode the visitor left.
@@ -210,6 +262,9 @@ function appearConfig(s: S, arg: unknown): AppearConfig {
210
262
  passwordDraft = "";
211
263
  }
212
264
  signInScreenRequested ||= strField(arg, "signInScreen") || s.signInScreen;
265
+ const presentation = strField(arg, "signInPresentation");
266
+ if (presentation === "sheet" || presentation === "full_screen") signInPresentationRequested = presentation;
267
+ signInReturnTabRequested ||= strField(arg, "signInReturnTab");
213
268
  // Only the sign-in screen's route carries `mode`, so it marks the form from the boot load on.
214
269
  standaloneRequested ||= s.standalone || appearing || (!routeInputConsumed && !!routeInput().mode);
215
270
  pollIdentityRequested ||= s.pollIdentity || strField(arg, "pollIdentity") === "1";
@@ -676,6 +731,7 @@ const svc = defineService<S, {
676
731
  signOut: (s: S) => Promise<S>;
677
732
  sync: (s: S) => Promise<S | undefined>;
678
733
  openMember: (s: S, arg?: unknown) => Promise<S>;
734
+ leave: (s: S) => S;
679
735
  restorePublish: (s: S) => S;
680
736
  iapRestore: (s: S) => Promise<undefined>;
681
737
  }>("himiStoreAccount", {
@@ -756,7 +812,8 @@ const svc = defineService<S, {
756
812
  return "next" in r ? r.next : undefined;
757
813
  },
758
814
  // A row to a member-only screen shows to a guest too. A member opens it; a guest signs
759
- // in first and the sign-in screen replaces itself with it, so Back returns here.
815
+ // in first, and the sign-in screen then opens the row's screen — as a modal, by closing
816
+ // and pushing it on this tab (openSignIn) — so Back returns here either way.
760
817
  openMember: async (s, arg): Promise<S> => {
761
818
  const screenId = strField(arg, "screenId");
762
819
  if (!screenId) return s;
@@ -764,8 +821,10 @@ const svc = defineService<S, {
764
821
  // From the state as it is now: a probe may have landed during the identity read.
765
822
  const now = svc.getState();
766
823
  if (member) return navigate(now, { screenId });
767
- return navigate(now, { screenId: now.signInScreen || "himi_sign_in", args: { mode: "signin", returnScreenId: screenId } });
824
+ return openSignIn(now, { mode: "signin" }, { screenId });
768
825
  },
826
+ // The standalone form's Continue: leaves the form the way it was opened.
827
+ leave: (s) => navigate(s, { presentation: leaveNav() }),
769
828
  // Typing means any focus request was delivered: clear the tokens (safe while mounted, since
770
829
  // bumpToken never repeats), so a visit rarely ends — and a pooled form rarely reopens —
771
830
  // holding one.
@@ -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
+ }
@@ -2085,8 +2085,10 @@ const SCREENS = runtimeSetting({
2085
2085
  "actionId": "himi.open",
2086
2086
  "params": {
2087
2087
  "screenId": "himi_sign_in",
2088
- "mode": "signin"
2089
- }
2088
+ "mode": "signin",
2089
+ "modal": "1"
2090
+ },
2091
+ "presentation": "sheet"
2090
2092
  }
2091
2093
  }
2092
2094
  }
@@ -2104,8 +2106,10 @@ const SCREENS = runtimeSetting({
2104
2106
  "actionId": "himi.open",
2105
2107
  "params": {
2106
2108
  "screenId": "himi_sign_in",
2107
- "mode": "register"
2108
- }
2109
+ "mode": "register",
2110
+ "modal": "1"
2111
+ },
2112
+ "presentation": "sheet"
2109
2113
  }
2110
2114
  }
2111
2115
  }
@@ -3307,6 +3311,8 @@ const SCREENS = runtimeSetting({
3307
3311
  "memberAccountLinks": "1",
3308
3312
  "restorePurchases": "1",
3309
3313
  "signInScreen": "himi_sign_in",
3314
+ "signInPresentation": "sheet",
3315
+ "signInReturnTab": "profile",
3310
3316
  "pollIdentity": "1",
3311
3317
  "copy": "{\"account.subtitle\":\"Reservations, RSVPs and saved content in one place.\",\"storeAccount.authBody\":\"Sign in to continue — reservations and RSVPs stay with you.\",\"storeAccount.authBodyRegister\":\"Create a member account to continue — reservations and RSVPs stay with you.\"}"
3312
3318
  }
@@ -3562,8 +3568,10 @@ const SCREENS = runtimeSetting({
3562
3568
  "params": {
3563
3569
  "screenId": "himi_sign_in",
3564
3570
  "mode": "signin",
3565
- "returnScreenId": "himi_account_settings"
3566
- }
3571
+ "returnScreenId": "himi_account_settings",
3572
+ "modal": "1"
3573
+ },
3574
+ "presentation": "sheet"
3567
3575
  }
3568
3576
  }
3569
3577
  },
@@ -5473,8 +5481,7 @@ const SCREENS = runtimeSetting({
5473
5481
  "style": "primary",
5474
5482
  "fullWidth": true,
5475
5483
  "action": {
5476
- "actionId": "nav.back",
5477
- "presentation": "back"
5484
+ "actionId": "tier5.himiStoreAccount.leave"
5478
5485
  }
5479
5486
  }
5480
5487
  }
@@ -5515,7 +5522,7 @@ const SCREENS = runtimeSetting({
5515
5522
  "declaredCapabilities": [
5516
5523
  "wix_api"
5517
5524
  ],
5518
- "minBinaryVersion": "0.2.0",
5525
+ "minBinaryVersion": "1.7.0",
5519
5526
  "bundleVersion": "0.0.1"
5520
5527
  }
5521
5528
  },
@@ -6382,8 +6389,10 @@ const SCREENS = runtimeSetting({
6382
6389
  "screenId": "himi_sign_in",
6383
6390
  "mode": "signin",
6384
6391
  "intent": "profile",
6385
- "returnScreenId": "himi_view_profile"
6386
- }
6392
+ "returnScreenId": "himi_view_profile",
6393
+ "modal": "1"
6394
+ },
6395
+ "presentation": "sheet"
6387
6396
  }
6388
6397
  }
6389
6398
  }
@@ -7482,8 +7491,10 @@ const SCREENS = runtimeSetting({
7482
7491
  "screenId": "himi_sign_in",
7483
7492
  "mode": "signin",
7484
7493
  "intent": "addresses",
7485
- "returnScreenId": "himi_addresses"
7486
- }
7494
+ "returnScreenId": "himi_addresses",
7495
+ "modal": "1"
7496
+ },
7497
+ "presentation": "sheet"
7487
7498
  }
7488
7499
  }
7489
7500
  }
@@ -8539,8 +8550,10 @@ const SCREENS = runtimeSetting({
8539
8550
  "screenId": "himi_sign_in",
8540
8551
  "mode": "signin",
8541
8552
  "intent": "addresses",
8542
- "returnScreenId": "himi_address"
8543
- }
8553
+ "returnScreenId": "himi_address",
8554
+ "modal": "1"
8555
+ },
8556
+ "presentation": "sheet"
8544
8557
  }
8545
8558
  }
8546
8559
  }
@@ -9210,8 +9223,10 @@ const SCREENS = runtimeSetting({
9210
9223
  "screenId": "himi_sign_in",
9211
9224
  "mode": "signin",
9212
9225
  "intent": "wallet",
9213
- "returnScreenId": "himi_wallet"
9214
- }
9226
+ "returnScreenId": "himi_wallet",
9227
+ "modal": "1"
9228
+ },
9229
+ "presentation": "sheet"
9215
9230
  }
9216
9231
  }
9217
9232
  }
@@ -12432,8 +12447,10 @@ const SCREENS = runtimeSetting({
12432
12447
  "screenId": "himi_sign_in",
12433
12448
  "mode": "signin",
12434
12449
  "intent": "post",
12435
- "returnScreenId": "himi_create_post"
12436
- }
12450
+ "returnScreenId": "himi_create_post",
12451
+ "modal": "1"
12452
+ },
12453
+ "presentation": "sheet"
12437
12454
  }
12438
12455
  }
12439
12456
  }
@@ -19464,8 +19481,10 @@ const SCREENS = runtimeSetting({
19464
19481
  "params": {
19465
19482
  "screenId": "himi_sign_in",
19466
19483
  "intent": "food-orders",
19467
- "returnScreenId": "himi_food_orders"
19468
- }
19484
+ "returnScreenId": "himi_food_orders",
19485
+ "modal": "1"
19486
+ },
19487
+ "presentation": "sheet"
19469
19488
  }
19470
19489
  }
19471
19490
  }
@@ -20643,8 +20662,10 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
20643
20662
  "screenId": "himi_sign_in",
20644
20663
  "intent": "basket",
20645
20664
  "returnScreenId": "himi_menu_item",
20646
- "returnId": "$service:himiBrandedMenuItem.id"
20647
- }
20665
+ "returnId": "$service:himiBrandedMenuItem.id",
20666
+ "modal": "1"
20667
+ },
20668
+ "presentation": "sheet"
20648
20669
  },
20649
20670
  "leadingIcon": "person"
20650
20671
  }
@@ -22569,8 +22590,10 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
22569
22590
  "params": {
22570
22591
  "screenId": "himi_sign_in",
22571
22592
  "mode": "signin",
22572
- "intent": "rsvp"
22573
- }
22593
+ "intent": "rsvp",
22594
+ "modal": "1"
22595
+ },
22596
+ "presentation": "sheet"
22574
22597
  }
22575
22598
  }
22576
22599
  },
@@ -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