@wix/himalaya-cli 0.1570.0 → 0.1571.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/define-app.d.ts +8 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/auth-providers/wix/src/index.d.ts +7 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/blog-v3/src/extensions.d.ts +6 -3
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/auth.mjs +17 -7
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core/validation.mjs +17 -7
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +17 -7
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +17 -7
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/state.mjs +17 -7
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-auth.mjs +7 -1
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-blog.mjs +1 -0
  11. package/dist/cli.mjs +1672 -1206
  12. package/dist/preview-assets.generated.bin +0 -0
  13. package/guides/base44-app-to-himi-app.md +1 -1
  14. package/guides/capabilities.md +7 -0
  15. package/guides/himi-json-output.md +11 -0
  16. package/guides/one-app-to-himi-app.md +122 -4
  17. package/guides/sdk-reference.json +2 -2
  18. package/guides/splash-screens.md +10 -1
  19. package/guides/trim-a-template.md +5 -0
  20. package/guides/wix-business-context.md +11 -2
  21. package/package.json +2 -2
  22. package/templates/beauty-salon/config.ts +1 -1
  23. package/templates/beauty-salon/dev/index.ts +31 -1
  24. package/templates/beauty-salon/tier5-src/_shared/member-read.ts +52 -0
  25. package/templates/beauty-salon/tier5-src/_shared/program-description.ts +11 -0
  26. package/templates/beauty-salon/tier5-src/himi-blog-post/index.ts +35 -12
  27. package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +3 -3
  28. package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +6 -9
  29. package/templates/beauty-salon-owner/config.ts +1 -1
  30. package/templates/branded-lite/config.ts +1 -1
  31. package/templates/branded-lite/dev/index.ts +163 -2
  32. package/templates/branded-lite/tier5-src/_shared/member-read.ts +52 -0
  33. package/templates/branded-lite/tier5-src/_shared/program-description.ts +11 -0
  34. package/templates/branded-lite/tier5-src/himi-blog-post/index.ts +35 -12
  35. package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +3 -3
  36. package/templates/branded-lite/tier5-src/himi-branded-lite-program/index.ts +16 -14
  37. package/templates/branded-lite/tier5-src/himi-branded-lite-programs/index.ts +3 -7
  38. package/templates/branded-lite/tier5-src/himi-events-my-events/index.ts +76 -42
  39. package/templates/branded-lite/tier5-src/himi-store-product/index.ts +6 -9
  40. package/templates/branded-lite/tier5-src/wix-spaces-chat-thread/index.ts +101 -34
  41. package/templates/branded-lite/tier5-src/wix-spaces-forum-post/index.ts +23 -6
  42. package/templates/branded-lite/tier5-src/wix-spaces-member-detail/index.ts +26 -11
  43. package/templates/community-club/config.ts +1 -1
  44. package/templates/community-club/dev/index.ts +65 -2
  45. package/templates/community-club/tier5-src/_shared/branded-load-state.ts +106 -0
  46. package/templates/community-club/tier5-src/_shared/member-read.ts +52 -0
  47. package/templates/community-club/tier5-src/_shared/program-description.ts +11 -0
  48. package/templates/community-club/tier5-src/himi-blog-post/index.ts +35 -12
  49. package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +3 -3
  50. package/templates/community-club/tier5-src/himi-events-my-events/index.ts +76 -42
  51. package/templates/community-club-owner/config.ts +1 -1
  52. package/templates/events-meetups/config.ts +1 -1
  53. package/templates/events-meetups/dev/index.ts +65 -2
  54. package/templates/events-meetups/tier5-src/_shared/branded-load-state.ts +106 -0
  55. package/templates/events-meetups/tier5-src/_shared/member-read.ts +52 -0
  56. package/templates/events-meetups/tier5-src/_shared/program-description.ts +11 -0
  57. package/templates/events-meetups/tier5-src/himi-blog-post/index.ts +35 -12
  58. package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +3 -3
  59. package/templates/events-meetups/tier5-src/himi-events-my-events/index.ts +76 -42
  60. package/templates/events-meetups-owner/config.ts +1 -1
  61. package/templates/gym-fitness/config.ts +1 -1
  62. package/templates/gym-fitness/dev/index.ts +65 -2
  63. package/templates/gym-fitness/tier5-src/_shared/member-read.ts +52 -0
  64. package/templates/gym-fitness/tier5-src/_shared/program-description.ts +11 -0
  65. package/templates/gym-fitness/tier5-src/himi-blog-post/index.ts +35 -12
  66. package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +3 -3
  67. package/templates/gym-fitness/tier5-src/himi-events-my-events/index.ts +76 -42
  68. package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +6 -9
  69. package/templates/gym-fitness-owner/config.ts +1 -1
  70. package/templates/nonprofit-cause/config.ts +1 -1
  71. package/templates/nonprofit-cause/dev/index.ts +65 -2
  72. package/templates/nonprofit-cause/tier5-src/_shared/branded-load-state.ts +106 -0
  73. package/templates/nonprofit-cause/tier5-src/_shared/member-read.ts +52 -0
  74. package/templates/nonprofit-cause/tier5-src/_shared/program-description.ts +11 -0
  75. package/templates/nonprofit-cause/tier5-src/himi-blog-post/index.ts +35 -12
  76. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +3 -3
  77. package/templates/nonprofit-cause/tier5-src/himi-events-my-events/index.ts +76 -42
  78. package/templates/nonprofit-cause-owner/config.ts +1 -1
  79. package/templates/owner-lite/config.ts +1 -1
  80. package/templates/podcast-creator/config.ts +1 -1
  81. package/templates/podcast-creator/dev/index.ts +65 -2
  82. package/templates/podcast-creator/tier5-src/_shared/branded-load-state.ts +106 -0
  83. package/templates/podcast-creator/tier5-src/_shared/member-read.ts +52 -0
  84. package/templates/podcast-creator/tier5-src/_shared/program-description.ts +11 -0
  85. package/templates/podcast-creator/tier5-src/himi-blog-post/index.ts +35 -12
  86. package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +3 -3
  87. package/templates/podcast-creator/tier5-src/himi-events-my-events/index.ts +76 -42
  88. package/templates/podcast-creator-owner/config.ts +1 -1
  89. package/templates/portfolio-creative/config.ts +1 -1
  90. package/templates/portfolio-creative/dev/index.ts +65 -2
  91. package/templates/portfolio-creative/tier5-src/_shared/branded-load-state.ts +106 -0
  92. package/templates/portfolio-creative/tier5-src/_shared/member-read.ts +52 -0
  93. package/templates/portfolio-creative/tier5-src/_shared/program-description.ts +11 -0
  94. package/templates/portfolio-creative/tier5-src/himi-blog-post/index.ts +35 -12
  95. package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +3 -3
  96. package/templates/portfolio-creative/tier5-src/himi-events-my-events/index.ts +76 -42
  97. package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +6 -9
  98. package/templates/portfolio-creative-owner/config.ts +1 -1
  99. package/templates/restaurant/config.ts +1 -1
  100. package/templates/restaurant/dev/index.ts +65 -2
  101. package/templates/restaurant/tier5-src/_shared/branded-load-state.ts +106 -0
  102. package/templates/restaurant/tier5-src/_shared/member-read.ts +52 -0
  103. package/templates/restaurant/tier5-src/_shared/program-description.ts +11 -0
  104. package/templates/restaurant/tier5-src/himi-blog-post/index.ts +35 -12
  105. package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +3 -3
  106. package/templates/restaurant/tier5-src/himi-events-my-events/index.ts +76 -42
  107. package/templates/restaurant-owner/config.ts +1 -1
  108. package/templates/retail-store/config.ts +1 -1
  109. package/templates/retail-store/dev/index.ts +31 -1
  110. package/templates/retail-store/tier5-src/_shared/branded-load-state.ts +106 -0
  111. package/templates/retail-store/tier5-src/_shared/member-read.ts +52 -0
  112. package/templates/retail-store/tier5-src/_shared/program-description.ts +11 -0
  113. package/templates/retail-store/tier5-src/himi-blog-post/index.ts +35 -12
  114. package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +3 -3
  115. package/templates/retail-store/tier5-src/himi-store-product/index.ts +6 -9
  116. package/templates/retail-store-owner/config.ts +1 -1
  117. package/templates/yoga-studio/config.ts +1 -1
  118. package/templates/yoga-studio/dev/index.ts +93 -2
  119. package/templates/yoga-studio/tier5-src/_shared/member-read.ts +52 -0
  120. package/templates/yoga-studio/tier5-src/_shared/program-description.ts +11 -0
  121. package/templates/yoga-studio/tier5-src/himi-blog-post/index.ts +35 -12
  122. package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +3 -3
  123. package/templates/yoga-studio/tier5-src/himi-branded-lite-program/index.ts +16 -14
  124. package/templates/yoga-studio/tier5-src/himi-branded-lite-programs/index.ts +3 -7
  125. package/templates/yoga-studio/tier5-src/himi-events-my-events/index.ts +76 -42
  126. package/templates/yoga-studio-owner/config.ts +1 -1
@@ -5,6 +5,8 @@ import { queryEvents, type WixEvent } from "@himalaya/wix-events";
5
5
  import { getAll, upsert, type DbEntity } from "@himalaya/local-db";
6
6
  import { confirmedRsvpIds, eventStartMillis, normalizeEvent, type EventRow } from "../_shared/himi-events";
7
7
  import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
8
+ import { memberAccess } from "../_shared/member-access.js";
9
+ import { readAccess, assertReadAccess, readAccessDenied, readSignInFlags, NO_READ_SIGN_IN } from "../_shared/member-read.js";
8
10
 
9
11
  // Offline-first (data-offline): My Events is a read-through over the same `events`
10
12
  // catalog the list caches — paint the last-synced rows (filtered to this device's
@@ -27,6 +29,7 @@ async function writeCached(rows: CachedEventRow[]): Promise<void> {
27
29
  }
28
30
 
29
31
  interface S {
32
+ signInRequired: boolean; noSignIn: boolean; signInScreenId: string;
30
33
  status: "loading" | "ready";
31
34
  events: EventRow[];
32
35
  emptyText: string;
@@ -98,11 +101,10 @@ function unverifiedEvent(event: CachedEventRow): CachedEventRow {
98
101
  };
99
102
  }
100
103
 
101
- /** Keep only the candidate rows the member ACTUALLY still holds an RSVP for, asking
102
- * Wix per event. Fail-open per row: a network/permission blip must not blank a row the
103
- * member really has (that would look like their RSVP vanished), so an errored probe
104
- * keeps the row and only an explicit "no active RSVP" drops it. */
105
- async function confirmRsvps(candidates: CachedEventRow[]): Promise<{ events: CachedEventRow[]; verificationUnavailable: boolean }> {
104
+ /** Verify device-local candidates against the current member. Member-only mode
105
+ * requires successful verification; standalone mode retains failed probes as
106
+ * explicitly unverified device-local history. */
107
+ async function confirmRsvps(candidates: CachedEventRow[], memberOnly = false): Promise<{ events: CachedEventRow[]; verificationUnavailable: boolean }> {
106
108
  // Raw wixApi chokepoint rather than a generated client: the member RSVP read lives in
107
109
  // events **v2**, and `@himalaya/wix-events` is the v1 domain module. Adding a
108
110
  // `wix-events-v2` specifier is not an option — the umbrella package deliberately
@@ -122,7 +124,9 @@ async function confirmRsvps(candidates: CachedEventRow[]): Promise<{ events: Cac
122
124
  event: held ? { ...event, status: [event.status, copyParam("myEvents.rsvpConfirmed", "RSVP confirmed")].filter(Boolean).join(" · ") } : null,
123
125
  verificationUnavailable: false,
124
126
  };
125
- } catch {
127
+ } catch (error) {
128
+ // Device candidates carry no member ownership proof until this read succeeds.
129
+ if (memberOnly) throw error;
126
130
  // Fail-open, but never claim backend confirmation while its member-readable
127
131
  // probe is unavailable. The device id remains a candidate, explicitly labeled.
128
132
  return { event: unverifiedEvent(event), verificationUnavailable: true };
@@ -134,8 +138,8 @@ async function confirmRsvps(candidates: CachedEventRow[]): Promise<{ events: Cac
134
138
  };
135
139
  }
136
140
 
137
- const svc = defineService<S, { load: (s: S, arg?: unknown) => Promise<S> }>("himiMyEvents", {
138
- state: derive({
141
+ const INITIAL: S = derive({
142
+ ...NO_READ_SIGN_IN, signInScreenId: "himi_account",
139
143
  status: "loading",
140
144
  events: [],
141
145
  emptyText: "",
@@ -147,45 +151,75 @@ const svc = defineService<S, { load: (s: S, arg?: unknown) => Promise<S> }>("him
147
151
  isLoading: true,
148
152
  notLoading: false,
149
153
  noEmpty: true,
150
- }),
154
+ });
155
+ let memberOnlyRequested = false;
156
+ let loadVersion = 0;
157
+ const svc = defineService<S, { prepare: (s: S) => S; load: (s: S, arg?: unknown) => Promise<S | undefined> }>("himiMyEvents", {
158
+ state: INITIAL,
151
159
  actions: {
152
- load: async (s, arg) => {
160
+ prepare: (s) => ({ ...s, ...INITIAL }),
161
+ load: async (s, arg): Promise<S | undefined> => {
153
162
  stashCopy(arg);
154
- const confirmed = new Set(confirmedRsvpIds());
155
- const identity = await identityLine();
156
- // 1) Instant paint from the on-device cache (offline-safe; empty on first-ever run).
157
- const cached = await readCached();
158
- const cachedEvents = cached.filter((event) => confirmed.has(event.id)).map(displayEventRow);
159
- const base = cached.length
160
- ? derive({ ...s, status: "ready", errored: false, events: cachedEvents, identityLine: identity, verificationUnavailable: cachedEvents.length > 0 })
161
- : s;
162
- // 2) Revalidate from Wix and refresh the cache.
163
+ const version = ++loadVersion;
164
+ const options = arg as { memberOnly?: unknown; signInScreenId?: unknown } | undefined;
165
+ if (options?.memberOnly !== undefined) memberOnlyRequested = options.memberOnly === "1";
166
+ const memberOnly = memberOnlyRequested;
167
+ const clean = { ...INITIAL, signInScreenId: options?.signInScreenId === "himi_sign_in" ? "himi_sign_in" : s.signInScreenId };
168
+ await svc.prepare();
163
169
  try {
164
- const res = await queryEvents({ limit: 50 });
165
- const all = ((res as unknown as { events?: WixEvent[] }).events ?? []).map(cachedEventRow);
166
- await writeCached(all);
167
- // Phase 3c — make each row AUTHORITATIVE. The device list is only a candidate
168
- // set: it never forgets, so an RSVP cancelled elsewhere (web, or by the
169
- // business) would otherwise show here forever. Confirm each candidate against
170
- // Wix and drop the ones the member no longer holds.
171
- //
172
- // Per-event by necessity, not by choice: a member CANNOT enumerate their own
173
- // RSVPs. Verified live against a real member token — `queryRsvps` (cross-event)
174
- // 403s "WIX_EVENTS.READ_RSVP permission required", `listOrders` 403s
175
- // READ_ORDERS, and `/rsvps/current` 400s without an eventId. Only the
176
- // single-event read is member-readable, so the fan-out below IS the ceiling
177
- // until those member scopes exist server-side.
178
- const candidates = all.filter((event) => confirmed.has(event.id)).map(displayEventRow);
179
- const confirmation = await confirmRsvps(candidates);
180
- return derive({ ...base, status: "ready", errored: false, events: confirmation.events, identityLine: identity, verificationUnavailable: confirmation.verificationUnavailable });
181
- } catch {
182
- // Offline / fetch failure: keep the cached rows instead of a blank error state.
183
- return cached.length
184
- ? base
185
- : derive({ ...s, status: "ready", errored: true, events: [], identityLine: identity });
170
+ const member = memberOnly ? await readAccess() : null;
171
+ if (memberOnly && !member?.member.email) throw new Error("Unauthenticated: sign in to see your events");
172
+ const confirmed = new Set(confirmedRsvpIds());
173
+ const identity = await identityLine();
174
+ // 1) Instant paint from the on-device cache (offline-safe; empty on first-ever run).
175
+ // Device-local candidates have no member ownership proof. Branded must
176
+ // verify them online rather than show a previous member's cached RSVP list.
177
+ const cached = memberOnly ? [] : await readCached();
178
+ const cachedEvents = cached.filter((event) => confirmed.has(event.id)).map(displayEventRow);
179
+ const base = cached.length
180
+ ? derive({ ...clean, status: "ready", errored: false, events: cachedEvents, identityLine: identity, verificationUnavailable: cachedEvents.length > 0 })
181
+ : clean;
182
+ // 2) Revalidate from Wix and refresh the cache.
183
+ try {
184
+ const res = await queryEvents({ limit: 50 });
185
+ const all = ((res as unknown as { events?: WixEvent[] }).events ?? []).map(cachedEventRow);
186
+ if (member) await assertReadAccess(member);
187
+ if (version !== loadVersion) return undefined;
188
+ await writeCached(all);
189
+ // Phase 3c — make each row AUTHORITATIVE. The device list is only a candidate
190
+ // set: it never forgets, so an RSVP cancelled elsewhere (web, or by the
191
+ // business) would otherwise show here forever. Confirm each candidate against
192
+ // Wix and drop the ones the member no longer holds.
193
+ //
194
+ // Per-event by necessity, not by choice: a member CANNOT enumerate their own
195
+ // RSVPs. Verified live against a real member token — `queryRsvps` (cross-event)
196
+ // 403s "WIX_EVENTS.READ_RSVP permission required", `listOrders` 403s
197
+ // READ_ORDERS, and `/rsvps/current` 400s without an eventId. Only the
198
+ // single-event read is member-readable, so the fan-out below IS the ceiling
199
+ // until those member scopes exist server-side.
200
+ const candidates = all.filter((event) => confirmed.has(event.id)).map(displayEventRow);
201
+ const confirmation = await confirmRsvps(candidates, memberOnly);
202
+ if (member) await assertReadAccess(member);
203
+ if (version !== loadVersion) return undefined;
204
+ return derive({ ...base, status: "ready", errored: false, events: confirmation.events, identityLine: identity, verificationUnavailable: confirmation.verificationUnavailable });
205
+ } catch (error) {
206
+ if (memberOnly && readAccessDenied(error)) throw error;
207
+ if (member) await assertReadAccess(member);
208
+ if (version !== loadVersion) return undefined;
209
+ // Offline / fetch failure: keep the cached rows instead of a blank error state.
210
+ return cached.length
211
+ ? base
212
+ : derive({ ...clean, status: "ready", errored: true, events: [], identityLine: identity });
213
+ }
214
+ } catch (error) {
215
+ const failure = memberOnly && !(await memberAccess()).email ? new Error("Unauthenticated: sign in to see your events") : error;
216
+ const flags = await readSignInFlags(failure);
217
+ return version === loadVersion ? derive({ ...clean, ...flags, status: "ready", errored: true, events: [] }) : undefined;
186
218
  }
187
219
  },
188
220
  },
189
221
  });
190
- svc.load();
222
+ // Wait for the descriptor's load action to select member-only or device-local scope.
223
+ // An eager unconfigured read could briefly expose guest KV before Branded opts in.
224
+ svc.prepare();
191
225
  export { svc };
@@ -19,7 +19,7 @@ import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
19
19
  import { isBridgeUnavailable } from "../_shared/owner-load-error.js";
20
20
  import { isProductSaved, toggleSavedProduct } from "../_shared/himi-store-saved.js";
21
21
  import { openRichLink } from "../_shared/rich-content-link.js";
22
- import { richBody } from "../_shared/rich-body.js";
22
+ import { descriptionBody } from "../_shared/rich-body.js";
23
23
 
24
24
  /** Live cart size (sum of line-item quantities) for the top-bar cart badge.
25
25
  * Best-effort: a cart error just yields 0 (badge hidden), never fails the screen. */
@@ -74,9 +74,7 @@ function productPolicyBody(product: StoreProduct, kind: "returns" | "shipping"):
74
74
  });
75
75
  if (!section) return "";
76
76
  const html = "plainDescription" in section ? section.plainDescription : undefined;
77
- if (html) return plainText(html.replace(/&nbsp;/gi, " "));
78
- if (typeof section.description === "string") return plainText(section.description.replace(/&nbsp;/gi, " "));
79
- return richBody(section.description).bodyText;
77
+ return descriptionBody(html || section.description).bodyText;
80
78
  }
81
79
 
82
80
  /** The product id `lastProduct` was read for. A pooled screen re-appearing calls
@@ -140,13 +138,12 @@ function optionsHint(groups: StoreOption[], selection: StoreOptionSelection): st
140
138
  return copyTemplate("product.optionsChoose", "Choose {options}", { options: missing.join(", ") });
141
139
  }
142
140
 
143
- /** The description pair. V3's `description` is a Ricos OBJECT (the HTML `plainText` would throw on it): it
144
- * becomes the RichContent document, and `plainDescription` (or the document's own text) the plain copy. A V1
145
- * `description` is an HTML string: stripped text, no document. */
141
+ /** Catalog V3 returns Ricos objects or serialized documents; V1 returns HTML.
142
+ * Keep plainDescription as the independent fallback, never raw document JSON. */
146
143
  function productBody(p: StoreProduct): { description: string; descriptionJson: string } {
147
144
  const raw = (p as { description?: unknown }).description;
148
- const html = plainText(p.plainDescription || (typeof raw === "string" ? raw : ""));
149
- const { bodyJson, bodyText } = richBody(typeof raw === "string" ? null : raw, html);
145
+ const html = descriptionBody(p.plainDescription).bodyText;
146
+ const { bodyJson, bodyText } = descriptionBody(raw, html);
150
147
  return { description: html || bodyText, descriptionJson: bodyJson };
151
148
  }
152
149
 
@@ -9,7 +9,7 @@
9
9
  // `chatRealtime` channel, else a 4 s foreground `_poll` of `refresh`. A message from the
10
10
  // business then appears without the member leaving the screen. `load`, `setDraft` and
11
11
  // `send` keep their contract; the directives are stripped by the runtime, never state.
12
- import { defineService, type PollDirective, type StreamDirective } from "@himalaya/state";
12
+ import { defineService, type PollDirective, type PollStopDirective, type StreamDirective } from "@himalaya/state";
13
13
  import { getMessages, sendChatMessage, getOrCreateDirectChatroom, getChatToken, type ChatMessage } from "@himalaya/wix-chat-web";
14
14
  import { getMyData } from "@himalaya/wix-users";
15
15
  import { formatMonthDay, formatMonthDayTime } from "../_shared/engine-intl.js";
@@ -17,7 +17,10 @@ import { initials } from "../_shared/himi-text.js";
17
17
  import { copyParam, stashCopy } from "../_shared/himi-copy.js";
18
18
  import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
19
19
  import { writeMayHaveLanded } from "../_shared/owner-load-error.js";
20
- import { chatRealtimeConfig, createReloadGate, createSendIntents, createVisitToken, liveDirectives, postWithVisitToken, liveFallback, type ChatLiveMode, type ChatRealtimeConfig } from "../_shared/chat-live.js";
20
+ import { CHAT_LIVE_POLL_ID, chatRealtimeConfig, createReloadGate, createSendIntents, createVisitToken, liveDirectives, postWithVisitToken, isTokenRejected, liveFallback, type ChatLiveMode, type ChatRealtimeConfig } from "../_shared/chat-live.js";
21
+
22
+ import { memberAccess, memberAccessChanged } from "../_shared/member-access.js";
23
+ import { readAccess, assertReadAccess, readAccessDenied, NO_READ_SIGN_IN, type ReadAccess } from "../_shared/member-read.js";
21
24
 
22
25
  const SERVICE = "spacesChatThread";
23
26
  const EMPTY_TEXT = "No messages yet. Say hello!";
@@ -31,13 +34,14 @@ interface Row {
31
34
  imageUrl: string; imageDescription: string; imageHidden: boolean; dayLabel: string; dayHidden: boolean;
32
35
  }
33
36
  interface S extends PresenceFlags {
37
+ threadMember: ReadAccess | null; signInRequired: boolean; noSignIn: boolean;
34
38
  status: "loading" | "ready"; messages: Row[]; emptyText: string; emptyHidden: boolean; draft: string; sendHint: string; resolvedChatroomId: string; myUserId: string;
35
39
  /** Send token for the host's transcript scroll: a new value follows the tail. */
36
40
  scrollSend: string;
37
41
  /** Which path keeps the open thread live (diagnostic). */
38
42
  live?: ChatLiveMode;
39
43
  _stream?: StreamDirective;
40
- _poll?: PollDirective;
44
+ _poll?: PollDirective | PollStopDirective;
41
45
  }
42
46
 
43
47
  function inputId(): string {
@@ -125,15 +129,21 @@ function nextScrollToken(prev: string): string {
125
129
  // mint a token every 4 s on top of its GET. Reset on every `load`.
126
130
  const memberToken = createVisitToken(() => getChatToken({ role: "member" }).then((t) => t.chatToken));
127
131
 
128
- async function reload(id: string, myUserId: string): Promise<Row[]> {
132
+ async function reload(id: string, myUserId: string, member: ReadAccess): Promise<Row[]> {
129
133
  if (!id) return [];
130
- const token = memberToken.get();
131
- try {
132
- const msgs = await getMessages(id, 30, { role: "member", chatToken: await token });
133
- return rowsOf(msgs ?? [], myUserId);
134
- } catch (e) {
135
- memberToken.drop(token);
136
- throw e;
134
+ for (let attempt = 0; ; attempt++) {
135
+ const token = memberToken.get();
136
+ try {
137
+ const chatToken = await token;
138
+ await assertReadAccess(member);
139
+ const msgs = await getMessages(id, 30, { role: "member", chatToken });
140
+ return rowsOf(msgs ?? [], myUserId);
141
+ } catch (error) {
142
+ memberToken.drop(token);
143
+ // An expired visit token gets one fresh attempt. A repeated refusal still
144
+ // clears private history; checking identity before each read prevents stale retries.
145
+ if (attempt > 0 || !isTokenRejected(error)) throw error;
146
+ }
137
147
  }
138
148
  }
139
149
 
@@ -142,9 +152,13 @@ const intents = createSendIntents();
142
152
 
143
153
  /** Post once per intent: a retried tap of the same text reuses its message id, so a send
144
154
  * that failed ambiguously and is tapped again stays one message. */
145
- async function postOnce(chatroomId: string, text: string): Promise<void> {
155
+ async function postOnce(chatroomId: string, text: string, member: ReadAccess): Promise<void> {
146
156
  const messageId = intents.idFor(chatroomId, text);
147
- await postWithVisitToken(memberToken, (chatToken) => sendChatMessage(chatroomId, text, { role: "member", chatToken, messageId }));
157
+ await postWithVisitToken(memberToken, async (chatToken) => {
158
+ await assertReadAccess(member);
159
+ if (liveState().threadMember !== member) throw memberAccessChanged();
160
+ return sendChatMessage(chatroomId, text, { role: "member", chatToken, messageId });
161
+ });
148
162
  intents.settle(chatroomId, text);
149
163
  }
150
164
 
@@ -155,7 +169,7 @@ let sendingNow = false;
155
169
  // The conversation a live re-read reads, and the channel the screen passed (if any).
156
170
  let current = { chatroomId: "", myUserId: "" };
157
171
  let realtime: ChatRealtimeConfig | null = null;
158
- const gate = createReloadGate(() => reload(current.chatroomId, current.myUserId));
172
+ const gate = createReloadGate(() => reload(current.chatroomId, current.myUserId, liveState().threadMember!));
159
173
 
160
174
  /** A tick or a stream event: re-read the open conversation. A failed tick keeps the
161
175
  * thread as it is — the next one tries again.
@@ -167,12 +181,16 @@ const gate = createReloadGate(() => reload(current.chatroomId, current.myUserId)
167
181
  async function reread(s: S): Promise<S> {
168
182
  if (!s.found || !s.resolvedChatroomId || s.resolvedChatroomId !== current.chatroomId) return s;
169
183
  try {
184
+ await assertThreadMember(s);
170
185
  const messages = await gate.request();
186
+ await assertThreadMember(s);
171
187
  const now = svc.getState();
172
- if (now.resolvedChatroomId !== s.resolvedChatroomId) return now;
188
+ if (now.threadMember !== s.threadMember || now.resolvedChatroomId !== s.resolvedChatroomId) return now;
173
189
  return { ...now, messages, emptyText: messages.length ? "" : now.emptyText || EMPTY_TEXT, emptyHidden: messages.length > 0 };
174
- } catch {
175
- return svc.getState();
190
+ } catch (error) {
191
+ const now = svc.getState();
192
+ if (now.threadMember !== s.threadMember) return now;
193
+ return readAccessDenied(error) ? threadFailure(error) : now;
176
194
  }
177
195
  }
178
196
 
@@ -187,40 +205,71 @@ async function resolveMyUserId(): Promise<string> {
187
205
  try { return (await getMyData()).userId || ""; } catch { return ""; }
188
206
  }
189
207
 
208
+ const INITIAL: S = { status: "loading", messages: [], emptyText: "", emptyHidden: true, scrollSend: "", draft: "", sendHint: "", resolvedChatroomId: "", myUserId: "", ...presence("loading"), ...NO_READ_SIGN_IN, threadMember: null };
209
+ let loadVersion = 0;
210
+
211
+ async function threadFailure(error: unknown): Promise<S> {
212
+ const version = loadVersion;
213
+ const member = await memberAccess();
214
+ const flags = member.email ? NO_READ_SIGN_IN : { signInRequired: true, noSignIn: false };
215
+ if (version !== loadVersion) return liveState();
216
+ current = { chatroomId: "", myUserId: "" };
217
+ memberToken.reset();
218
+ return { ...INITIAL, status: "ready", ...presenceAfterError(error), ...flags, live: "off", _poll: { id: CHAT_LIVE_POLL_ID, stop: true } };
219
+ }
220
+ async function assertThreadMember(s: S): Promise<void> {
221
+ if (!s.threadMember?.member?.email) throw new Error("Unauthenticated: sign in to read messages");
222
+ await assertReadAccess(s.threadMember);
223
+ }
224
+
190
225
  const svc = defineService<S, {
191
- load: (s: S, a?: unknown) => Promise<S>;
226
+ prepare: (s: S) => S;
227
+ load: (s: S, a?: unknown) => Promise<S | undefined>;
192
228
  setDraft: (s: S, a?: unknown) => S;
193
229
  send: (s: S) => Promise<S>;
194
230
  refresh: (s: S) => Promise<S>;
195
231
  onStreamEvent: (s: S) => Promise<S>;
196
232
  onStreamError: (s: S) => S;
197
233
  }>(SERVICE, {
198
- state: { status: "loading", messages: [], emptyText: "", emptyHidden: true, scrollSend: "", draft: "", sendHint: "", resolvedChatroomId: "", myUserId: "", ...presence("loading") },
234
+ state: INITIAL,
199
235
  actions: {
200
- load: async (s, a) => {
236
+ prepare: (s) => ({ ...s, ...INITIAL, _poll: { id: CHAT_LIVE_POLL_ID, stop: true } }),
237
+ load: async (s, a): Promise<S | undefined> => {
238
+ const version = ++loadVersion;
239
+ await svc.prepare();
240
+ current = { chatroomId: "", myUserId: "" };
201
241
  stashCopy(a);
202
242
  const param = (a as { chatRealtime?: unknown } | null | undefined)?.chatRealtime;
203
243
  if (param !== undefined) realtime = chatRealtimeConfig(param);
204
244
  const raw = inputId();
205
245
  // No thread / recipient id (a stale link, a route opened directly) -> the shared
206
246
  // not-found state, and no composer: there is nobody to send to.
207
- if (!detailInputId()) return { ...s, status: "ready", messages: [], emptyText: "", emptyHidden: true, ...presence("missing") };
247
+ if (!detailInputId()) return { ...INITIAL, status: "ready", messages: [], emptyText: "", emptyHidden: true, ...presence("missing") };
208
248
  memberToken.reset();
209
- // Resolve chatroom + my user id concurrently (myUserId tags own messages).
210
- const [chatroomId, myUserId] = await Promise.all([resolveChatroomId(raw, inputKind()), resolveMyUserId()]);
211
- // A recipient whose 1:1 chatroom could not be resolved is not a conversation yet:
212
- // reload("") would return [] without throwing and publish a live, empty thread
213
- // whose Send can only fail. Show the load-error line instead.
214
- if (!chatroomId) return { ...s, status: "ready", resolvedChatroomId: "", myUserId, messages: [], emptyText: "", emptyHidden: true, ...presence("failed") };
215
249
  try {
216
- const messages = await reload(chatroomId, myUserId);
250
+ const member = await readAccess();
251
+ if (!member.member.email) throw new Error("Unauthenticated: sign in to read messages");
252
+ // Resolve chatroom + my user id concurrently (myUserId tags own messages).
253
+ const [chatroomId, myUserId] = await Promise.all([resolveChatroomId(raw, inputKind()), resolveMyUserId()]);
254
+ // A recipient whose 1:1 chatroom could not be resolved is not a conversation yet:
255
+ // reload("") would return [] without throwing and publish a live, empty thread
256
+ // whose Send can only fail. Show the load-error line instead.
257
+ if (!chatroomId) return version === loadVersion ? { ...INITIAL, status: "ready", ...presence("failed") } : undefined;
258
+ const messages = await reload(chatroomId, myUserId, member);
259
+ await assertReadAccess(member);
260
+ if (version !== loadVersion) return undefined;
217
261
  current = { chatroomId, myUserId };
218
- return { ...s, status: "ready", resolvedChatroomId: chatroomId, myUserId, messages, emptyText: messages.length ? "" : EMPTY_TEXT, emptyHidden: messages.length > 0, scrollSend: messages.length ? nextScrollToken(s.scrollSend) : "", ...presence("found"), ...liveDirectives(realtime, chatroomId, SERVICE) };
262
+ const previous = s.threadMember;
263
+ const sameReader = previous?.member.email === member.member.email && previous?.member.scope === member.member.scope && previous?.tenant === member.tenant;
264
+ const draft = sameReader && s.resolvedChatroomId === chatroomId ? s.draft : "";
265
+ return { ...INITIAL, draft, threadMember: member, status: "ready", resolvedChatroomId: chatroomId, myUserId, messages, emptyText: messages.length ? "" : EMPTY_TEXT, emptyHidden: messages.length > 0, scrollSend: messages.length ? nextScrollToken(s.scrollSend) : "", ...presence("found"), ...liveDirectives(realtime, chatroomId, SERVICE) };
219
266
  } catch (e) {
220
267
  // A thread whose messages cannot be read is not a live, empty conversation: a 404
221
268
  // is the shared not-found state, anything else the load-error line — never a
222
269
  // "Say hello!" composer that can only fail.
223
- return { ...s, status: "ready", resolvedChatroomId: chatroomId, myUserId, messages: [], emptyText: "", emptyHidden: true, ...presenceAfterError(e) };
270
+ if (version !== loadVersion) return undefined;
271
+ const failed = await threadFailure(e);
272
+ return version === loadVersion ? failed : undefined;
224
273
  }
225
274
  },
226
275
  setDraft: (s, a) => ({ ...s, draft: val(a) }),
@@ -234,9 +283,14 @@ const svc = defineService<S, {
234
283
  // Every return builds on the LIVE state: the runtime replaces state wholesale, and ticks
235
284
  // or typing may have changed it while this awaited.
236
285
  send: async (s) => {
286
+ if (!s.found) return s;
237
287
  const text = s.draft.trim();
238
- if (!text) return { ...s, sendHint: "Type a message first." };
239
- if (sendingNow) return s;
288
+ try { await assertThreadMember(s); }
289
+ catch (error) { return liveState().threadMember !== s.threadMember ? liveState() : threadFailure(error); }
290
+ if (liveState().threadMember !== s.threadMember) return liveState();
291
+ const checked = liveState();
292
+ if (!text) return checked.draft.trim() ? checked : { ...checked, sendHint: "Type a message first." };
293
+ if (sendingNow) return checked;
240
294
  sendingNow = true;
241
295
  try {
242
296
  // Resolve lazily in case send() runs before load() settled.
@@ -244,8 +298,11 @@ const svc = defineService<S, {
244
298
  if (!chatroomId) chatroomId = await resolveChatroomId(inputId(), inputKind());
245
299
  if (!chatroomId) return { ...liveState(), sendHint: "Couldn't start conversation." };
246
300
  try {
247
- await postOnce(chatroomId, text);
301
+ await postOnce(chatroomId, text, s.threadMember!);
248
302
  } catch (e) {
303
+ if (liveState().threadMember !== s.threadMember) return liveState();
304
+ try { await assertThreadMember(s); }
305
+ catch (error) { return threadFailure(error); }
249
306
  // Surface a short reason (the SDK rejects with "HTTP <code>: <body>") so a
250
307
  // real failure is diagnosable on-device instead of an opaque "Couldn't send".
251
308
  // A failure that may have posted anyway (a gateway timeout after the backend stored
@@ -255,6 +312,8 @@ const svc = defineService<S, {
255
312
  if (writeMayHaveLanded(e)) return { ...liveState(), resolvedChatroomId: chatroomId, sendHint: "Couldn't confirm it was sent — check before sending again." };
256
313
  return { ...liveState(), resolvedChatroomId: chatroomId, sendHint: msg ? `Couldn't send — ${msg.slice(0, 80)}` : "Couldn't send — try again." };
257
314
  }
315
+ await assertThreadMember(s);
316
+ if (liveState().threadMember !== s.threadMember) return liveState();
258
317
  // SENT. From here on nothing may say otherwise: a failed re-read must not offer a
259
318
  // second Send of a message that already posted.
260
319
  const myUserId = s.myUserId || (await resolveMyUserId());
@@ -264,18 +323,26 @@ const svc = defineService<S, {
264
323
  const cleared = (now: S): string => (now.draft.trim() === text ? "" : now.draft);
265
324
  try {
266
325
  const messages = await gate.request();
326
+ await assertThreadMember(s);
327
+ if (liveState().threadMember !== s.threadMember) return liveState();
267
328
  const now = liveState();
268
329
  return { ...now, resolvedChatroomId: chatroomId, myUserId, messages, draft: cleared(now), emptyText: "", emptyHidden: true, scrollSend: nextScrollToken(now.scrollSend), sendHint: "Sent ✓" };
269
- } catch {
330
+ } catch (error) {
331
+ if (liveState().threadMember !== s.threadMember) return liveState();
332
+ if (readAccessDenied(error)) return threadFailure(error);
270
333
  const now = liveState();
271
334
  const local: Row = { id: `local-${now.messages.length}`, text, when: "", leadFill: " ", mine: true, notMine: false, senderBadge: "", avatarUrl: "", avatarInitials: "", imageUrl: "", imageDescription: "", imageHidden: true, dayLabel: "", dayHidden: true };
272
335
  return { ...now, resolvedChatroomId: chatroomId, myUserId, messages: [...now.messages, local], draft: cleared(now), emptyText: "", emptyHidden: true, scrollSend: nextScrollToken(now.scrollSend), sendHint: "Sent ✓" };
273
336
  }
337
+ } catch (error) {
338
+ if (liveState().threadMember !== s.threadMember) return liveState();
339
+ return threadFailure(error);
274
340
  } finally {
275
341
  sendingNow = false;
276
342
  }
277
343
  },
278
344
  },
279
345
  });
346
+ svc.prepare();
280
347
  svc.load();
281
348
  export { svc };
@@ -7,7 +7,10 @@ import { presence, presenceAfterError, detailInputId, type PresenceFlags } from
7
7
  import { openRichLink } from "../_shared/rich-content-link.js";
8
8
  import { richBody } from "../_shared/rich-body.js";
9
9
 
10
+ import { readAccess, assertReadAccess, readSignInFlags, NO_READ_SIGN_IN } from "../_shared/member-read.js";
11
+
10
12
  interface S extends PresenceFlags {
13
+ signInRequired: boolean; noSignIn: boolean;
11
14
  status: "loading" | "ready"; id: string; title: string; author: string; subtitle: string;
12
15
  /** The plain-text body (`contentText`): RichContent's fallback, and what shows when there is no document. */
13
16
  body: string;
@@ -26,8 +29,12 @@ function val(arg?: unknown): string {
26
29
  return typeof arg === "string" ? arg : ((arg as { value?: string; text?: string })?.value ?? (arg as { text?: string })?.text ?? "");
27
30
  }
28
31
 
32
+ const INITIAL: S = { status: "loading", id: "", title: "", author: "", subtitle: "", body: "", bodyJson: "", hasBodyJson: false, initial: "", likeHint: "", commentText: "", commentHint: "", ...presence("loading"), ...NO_READ_SIGN_IN };
33
+ let loadVersion = 0;
34
+
29
35
  const svc = defineService<S, {
30
- load: (s: S) => Promise<S>;
36
+ prepare: (s: S) => S;
37
+ load: (s: S) => Promise<S | undefined>;
31
38
  like: (s: S) => Promise<S>;
32
39
  unlike: (s: S) => Promise<S>;
33
40
  setComment: (s: S, a?: unknown) => S;
@@ -35,12 +42,16 @@ const svc = defineService<S, {
35
42
  /** [rico] #3372 — RichContent's on_link: `{ url }` of the tapped link. */
36
43
  openLink: (s: S, a?: unknown) => Promise<S>;
37
44
  }>("spacesForumPost", {
38
- state: { status: "loading", id: "", title: "", author: "", subtitle: "", body: "", bodyJson: "", hasBodyJson: false, initial: "", likeHint: "", commentText: "", commentHint: "", ...presence("loading") },
45
+ state: INITIAL,
39
46
  actions: {
40
- load: async (s) => {
47
+ prepare: (s) => ({ ...s, ...INITIAL }),
48
+ load: async (s): Promise<S | undefined> => {
49
+ const version = ++loadVersion;
50
+ await svc.prepare();
41
51
  const id = detailInputId();
42
- if (!id) return { ...s, status: "ready", ...presence("missing") };
52
+ if (!id) return { ...INITIAL, status: "ready", ...presence("missing") };
43
53
  try {
54
+ const access = await readAccess();
44
55
  const res = await getPost(id);
45
56
  const p = ((res as unknown as { post?: WixPost }).post) ?? (res as unknown as WixPost);
46
57
  const pp = p as unknown as { title?: string; contentText?: string; content?: unknown; writer?: { name?: string }; commentCount?: number; viewCount?: number };
@@ -56,10 +67,15 @@ const svc = defineService<S, {
56
67
  // which is the document's own text when there is no contentText.
57
68
  const { bodyJson, bodyText } = richBody(doc, legacy);
58
69
  const body = legacy || bodyText || "No content.";
59
- return { status: "ready", id, title: pp.title || "Post", author, subtitle: bits.join(" · "),
70
+ await assertReadAccess(access);
71
+ if (version !== loadVersion) return undefined;
72
+ return { ...INITIAL, status: "ready", id, title: pp.title || "Post", author, subtitle: bits.join(" · "),
60
73
  body, bodyJson, hasBodyJson: bodyJson !== "", initial: (author[0] || "#").toUpperCase(),
61
74
  likeHint: "", commentText: "", commentHint: "", ...presence("found") };
62
- } catch (e) { return { ...s, status: "ready", id, ...presenceAfterError(e) }; }
75
+ } catch (e) {
76
+ const flags = await readSignInFlags(e);
77
+ return version === loadVersion ? { ...INITIAL, status: "ready", id, ...presenceAfterError(e), ...flags } : undefined;
78
+ }
63
79
  },
64
80
  // REAL WRITE: react to / un-react to the post.
65
81
  like: async (s) => {
@@ -85,5 +101,6 @@ const svc = defineService<S, {
85
101
  openLink: async (s, a) => ((await openRichLink(a)) ? s : { ...s, commentHint: "This link could not be opened." }),
86
102
  },
87
103
  });
104
+ svc.prepare();
88
105
  svc.load();
89
106
  export { svc };
@@ -14,7 +14,10 @@ import {
14
14
  } from "@himalaya/wix-members";
15
15
  import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
16
16
 
17
+ import { readAccess, assertReadAccess, readSignInFlags, NO_READ_SIGN_IN } from "../_shared/member-read.js";
18
+
17
19
  interface S extends PresenceFlags {
20
+ signInRequired: boolean; noSignIn: boolean;
18
21
  status: "loading" | "ready";
19
22
  id: string;
20
23
  name: string;
@@ -43,11 +46,7 @@ function followCountFor(followers: number, following: number): string {
43
46
  return followers + " Followers · " + following + " Following";
44
47
  }
45
48
 
46
- const svc = defineService<S, {
47
- load: (s: S) => Promise<S>;
48
- toggleFollow: (s: S) => Promise<S>;
49
- }>("spacesMemberDetail", {
50
- state: {
49
+ const INITIAL: S = {
51
50
  status: "loading",
52
51
  id: "",
53
52
  name: "",
@@ -61,13 +60,25 @@ const svc = defineService<S, {
61
60
  following: 0,
62
61
  followLabel: "Follow",
63
62
  followCount: "0 Followers · 0 Following",
64
- ...presence("loading"),
65
- },
63
+ ...presence("loading"), ...NO_READ_SIGN_IN,
64
+ };
65
+ let loadVersion = 0;
66
+
67
+ const svc = defineService<S, {
68
+ prepare: (s: S) => S;
69
+ load: (s: S) => Promise<S | undefined>;
70
+ toggleFollow: (s: S) => Promise<S>;
71
+ }>("spacesMemberDetail", {
72
+ state: INITIAL,
66
73
  actions: {
67
- load: async (s) => {
74
+ prepare: (s) => ({ ...s, ...INITIAL }),
75
+ load: async (s): Promise<S | undefined> => {
76
+ const version = ++loadVersion;
77
+ await svc.prepare();
68
78
  const id = detailInputId();
69
- if (!id) return { ...s, status: "ready", ...presence("missing") };
79
+ if (!id) return { ...INITIAL, status: "ready", ...presence("missing") };
70
80
  try {
81
+ const access = await readAccess();
71
82
  const res = await getMember(id);
72
83
  const m = ((res as unknown as { member?: WixMember }).member) ?? (res as unknown as WixMember);
73
84
  const n = name(m);
@@ -87,8 +98,10 @@ const svc = defineService<S, {
87
98
  isFollowing = await isFollowingMember(id);
88
99
  } catch { /* default false */ }
89
100
 
101
+ await assertReadAccess(access);
102
+ if (version !== loadVersion) return undefined;
90
103
  return {
91
- status: "ready",
104
+ ...INITIAL, status: "ready",
92
105
  id,
93
106
  name: n,
94
107
  email: m.loginEmail || "",
@@ -104,7 +117,8 @@ const svc = defineService<S, {
104
117
  ...presence("found"),
105
118
  };
106
119
  } catch (e) {
107
- return { ...s, status: "ready", id, ...presenceAfterError(e) };
120
+ const flags = await readSignInFlags(e);
121
+ return version === loadVersion ? { ...INITIAL, status: "ready", id, ...presenceAfterError(e), ...flags } : undefined;
108
122
  }
109
123
  },
110
124
  // REAL WRITE: two-state toggle. Flips the relationship and optimistically
@@ -144,5 +158,6 @@ const svc = defineService<S, {
144
158
  },
145
159
  },
146
160
  });
161
+ svc.prepare();
147
162
  svc.load();
148
163
  export { svc };
@@ -37,7 +37,7 @@ export default defineApp({
37
37
  actions: "http://localhost:8190/v1/actions",
38
38
  sync: "http://localhost:8190/v1/sync",
39
39
  },
40
- capabilities: ["live_activity","push","share","secure_storage","open_url","calendar","local_database","wix_api","wix_platform","app_info","iap","release_control","clipboard","theme"],
40
+ capabilities: ["push","share","secure_storage","open_url","calendar","local_database","wix_api","wix_platform","app_info","iap","release_control","clipboard","theme","wix_cashier"],
41
41
  cache: {
42
42
  "rules": [
43
43
  {