@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,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 './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
+ }
@@ -2158,8 +2158,10 @@ const SCREENS = runtimeSetting({
2158
2158
  "actionId": "himi.open",
2159
2159
  "params": {
2160
2160
  "screenId": "himi_sign_in",
2161
- "mode": "signin"
2162
- }
2161
+ "mode": "signin",
2162
+ "modal": "1"
2163
+ },
2164
+ "presentation": "sheet"
2163
2165
  }
2164
2166
  }
2165
2167
  }
@@ -2177,8 +2179,10 @@ const SCREENS = runtimeSetting({
2177
2179
  "actionId": "himi.open",
2178
2180
  "params": {
2179
2181
  "screenId": "himi_sign_in",
2180
- "mode": "register"
2181
- }
2182
+ "mode": "register",
2183
+ "modal": "1"
2184
+ },
2185
+ "presentation": "sheet"
2182
2186
  }
2183
2187
  }
2184
2188
  }
@@ -3298,6 +3302,8 @@ const SCREENS = runtimeSetting({
3298
3302
  "memberAccountLinks": "1",
3299
3303
  "restorePurchases": "1",
3300
3304
  "signInScreen": "himi_sign_in",
3305
+ "signInPresentation": "sheet",
3306
+ "signInReturnTab": "profile",
3301
3307
  "pollIdentity": "1",
3302
3308
  "copy": "{\"account.subtitle\":\"RSVPs, memberships and saved content in one place.\",\"storeAccount.authBody\":\"Sign in to continue — RSVPs and memberships stay with you.\",\"storeAccount.authBodyRegister\":\"Create a member account to continue — RSVPs and memberships stay with you.\"}"
3303
3309
  }
@@ -3553,8 +3559,10 @@ const SCREENS = runtimeSetting({
3553
3559
  "params": {
3554
3560
  "screenId": "himi_sign_in",
3555
3561
  "mode": "signin",
3556
- "returnScreenId": "himi_account_settings"
3557
- }
3562
+ "returnScreenId": "himi_account_settings",
3563
+ "modal": "1"
3564
+ },
3565
+ "presentation": "sheet"
3558
3566
  }
3559
3567
  }
3560
3568
  },
@@ -5464,8 +5472,7 @@ const SCREENS = runtimeSetting({
5464
5472
  "style": "primary",
5465
5473
  "fullWidth": true,
5466
5474
  "action": {
5467
- "actionId": "nav.back",
5468
- "presentation": "back"
5475
+ "actionId": "tier5.himiStoreAccount.leave"
5469
5476
  }
5470
5477
  }
5471
5478
  }
@@ -5506,7 +5513,7 @@ const SCREENS = runtimeSetting({
5506
5513
  "declaredCapabilities": [
5507
5514
  "wix_api"
5508
5515
  ],
5509
- "minBinaryVersion": "0.2.0",
5516
+ "minBinaryVersion": "1.7.0",
5510
5517
  "bundleVersion": "0.0.1"
5511
5518
  }
5512
5519
  },
@@ -6373,8 +6380,10 @@ const SCREENS = runtimeSetting({
6373
6380
  "screenId": "himi_sign_in",
6374
6381
  "mode": "signin",
6375
6382
  "intent": "profile",
6376
- "returnScreenId": "himi_view_profile"
6377
- }
6383
+ "returnScreenId": "himi_view_profile",
6384
+ "modal": "1"
6385
+ },
6386
+ "presentation": "sheet"
6378
6387
  }
6379
6388
  }
6380
6389
  }
@@ -7439,8 +7448,10 @@ const SCREENS = runtimeSetting({
7439
7448
  "screenId": "himi_sign_in",
7440
7449
  "mode": "signin",
7441
7450
  "intent": "wallet",
7442
- "returnScreenId": "himi_wallet"
7443
- }
7451
+ "returnScreenId": "himi_wallet",
7452
+ "modal": "1"
7453
+ },
7454
+ "presentation": "sheet"
7444
7455
  }
7445
7456
  }
7446
7457
  }
@@ -7908,11 +7919,7 @@ const SCREENS = runtimeSetting({
7908
7919
  "size": "sm",
7909
7920
  "fullWidth": true,
7910
7921
  "action": {
7911
- "actionId": "himi.open",
7912
- "params": {
7913
- "screenId": "$service:himiPlaceSummary.ctaScreenId",
7914
- "mode": "$service:himiPlaceSummary.ctaMode"
7915
- }
7922
+ "actionId": "tier5.himiPlaceSummary.openCta"
7916
7923
  }
7917
7924
  }
7918
7925
  },
@@ -7953,7 +7960,8 @@ const SCREENS = runtimeSetting({
7953
7960
  "onAppear": {
7954
7961
  "actionId": "tier5.himiPlaceSummary.load",
7955
7962
  "params": {
7956
- "verticals": "[\"plans\",\"feed\",\"events\",\"blog\",\"podcasts\"]"
7963
+ "verticals": "[\"plans\",\"feed\",\"events\",\"blog\",\"podcasts\"]",
7964
+ "signInPresentation": "sheet"
7957
7965
  }
7958
7966
  }
7959
7967
  }
@@ -10899,8 +10907,10 @@ const SCREENS = runtimeSetting({
10899
10907
  "actionId": "himi.open",
10900
10908
  "params": {
10901
10909
  "screenId": "himi_sign_in",
10902
- "mode": "signin"
10903
- }
10910
+ "mode": "signin",
10911
+ "modal": "1"
10912
+ },
10913
+ "presentation": "sheet"
10904
10914
  }
10905
10915
  }
10906
10916
  },
@@ -11400,8 +11410,10 @@ const SCREENS = runtimeSetting({
11400
11410
  "screenId": "himi_sign_in",
11401
11411
  "mode": "signin",
11402
11412
  "intent": "post",
11403
- "returnScreenId": "himi_feed"
11404
- }
11413
+ "returnScreenId": "himi_feed",
11414
+ "modal": "1"
11415
+ },
11416
+ "presentation": "sheet"
11405
11417
  }
11406
11418
  }
11407
11419
  }
@@ -12018,8 +12030,10 @@ const SCREENS = runtimeSetting({
12018
12030
  "screenId": "himi_sign_in",
12019
12031
  "mode": "signin",
12020
12032
  "intent": "post",
12021
- "returnScreenId": "himi_create_post"
12022
- }
12033
+ "returnScreenId": "himi_create_post",
12034
+ "modal": "1"
12035
+ },
12036
+ "presentation": "sheet"
12023
12037
  }
12024
12038
  }
12025
12039
  }
@@ -19315,8 +19329,10 @@ const SCREENS = runtimeSetting({
19315
19329
  "action": {
19316
19330
  "actionId": "himi.open",
19317
19331
  "params": {
19318
- "screenId": "himi_sign_in"
19319
- }
19332
+ "screenId": "himi_sign_in",
19333
+ "modal": "1"
19334
+ },
19335
+ "presentation": "sheet"
19320
19336
  }
19321
19337
  }
19322
19338
  },
@@ -23676,8 +23692,10 @@ const TEMPLATED: { pattern: string; display: string; liveSeed?: LiveSeed; static
23676
23692
  "params": {
23677
23693
  "screenId": "himi_sign_in",
23678
23694
  "mode": "signin",
23679
- "intent": "rsvp"
23680
- }
23695
+ "intent": "rsvp",
23696
+ "modal": "1"
23697
+ },
23698
+ "presentation": "sheet"
23681
23699
  }
23682
23700
  }
23683
23701
  },
@@ -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;
@@ -9,7 +9,7 @@
9
9
  // `load` is both the onAppear action and what an embed controller's `refresh()` runs (arg-less:
10
10
  // the stashed appear params keep the verticals and the copy pack), so a host that hears
11
11
  // `booking.created` / `booking.cancelled` re-reads the summary without a remount.
12
- import { defineService } from "@himalaya/state";
12
+ import { defineService, navigate } from "@himalaya/state";
13
13
  import { listMyPlanOrders, type WixPlanOrder } from "@himalaya/wix-pricing-plans";
14
14
  import { fetchMemberBookings, type MemberBookingRow } from "../_shared/member-bookings.js";
15
15
  import { memberAccess, assertMemberAccess, memberAccessDenied, type MemberAccess } from "../_shared/member-access.js";
@@ -52,6 +52,7 @@ const PLANS_SCREEN = "himi_plans";
52
52
  const SIGN_IN_SCREEN = "himi_sign_in";
53
53
 
54
54
  let stashedVerticals: string[] | null = null;
55
+ let signInPresentation: "sheet" | "full_screen" | undefined;
55
56
  let generation = 0;
56
57
 
57
58
  function appearVerticals(arg: unknown): string[] {
@@ -190,6 +191,8 @@ async function read(arg: unknown, again = true): Promise<PlaceSummaryState> {
190
191
  * write back over the newer result. */
191
192
  async function load(_s: PlaceSummaryState, arg?: unknown): Promise<PlaceSummaryState | undefined> {
192
193
  stashCopy(arg);
194
+ const presentation = arg && typeof arg === "object" ? (arg as Record<string, unknown>).signInPresentation : undefined;
195
+ if (presentation === "sheet" || presentation === "full_screen") signInPresentation = presentation;
193
196
  const mine = ++generation;
194
197
  const next = await read(arg);
195
198
  return mine === generation ? next : undefined;
@@ -198,11 +201,19 @@ async function load(_s: PlaceSummaryState, arg?: unknown): Promise<PlaceSummaryS
198
201
  const svc = defineService<PlaceSummaryState, {
199
202
  load: (s: PlaceSummaryState, arg?: unknown) => Promise<PlaceSummaryState | undefined>;
200
203
  retry: (s: PlaceSummaryState) => Promise<PlaceSummaryState | undefined>;
204
+ openCta: (s: PlaceSummaryState) => PlaceSummaryState;
201
205
  }>("himiPlaceSummary", {
202
206
  state: BLANK,
203
207
  actions: {
204
208
  load,
205
209
  retry: (s) => load(s),
210
+ openCta: (s) => {
211
+ if (s.hideCta || !s.ctaScreenId) return s;
212
+ // No return path: the tile is embedded, so the form closes over its host.
213
+ return navigate(s, s.ctaScreenId === SIGN_IN_SCREEN && signInPresentation
214
+ ? { screenId: SIGN_IN_SCREEN, presentation: signInPresentation, args: { mode: "signin", modal: "1" } }
215
+ : { screenId: s.ctaScreenId, args: { mode: s.ctaMode } });
216
+ },
206
217
  },
207
218
  });
208
219
  export { svc };
@@ -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: 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>;