@wix/himalaya-cli 0.1632.0 → 0.1634.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 (95) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments-contract.d.ts +4 -14
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments-wire.d.ts +2 -1
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments.d.ts +1 -2
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/apple-iap-contract.d.ts +26 -13
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/apple-iap-core.d.ts +0 -2
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/checkout-source.d.ts +3 -10
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/commerce-core.d.ts +0 -2
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/iap-order-backend.d.ts +0 -3
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/iap-verticals/stores.d.ts +2 -2
  10. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/index.d.ts +2 -2
  11. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/loyalty/src/index.d.ts +64 -0
  12. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/members-feed-viewer/src/index.d.ts +10 -6
  13. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/members-v1/src/index.d.ts +36 -0
  14. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +13 -45
  15. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +2 -4
  16. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +7 -11
  17. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-loyalty.mjs +39 -0
  18. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-members-feed-viewer.mjs +8 -7
  19. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-members.mjs +29 -0
  20. package/dist/browser-plane/assets.json +166 -0
  21. package/dist/browser-plane/host.html +156 -0
  22. package/dist/browser-plane/package-reader.js +4 -0
  23. package/dist/browser-plane/preview-deferred/368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5.bin +0 -0
  24. package/dist/browser-plane/preview-deferred/37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29.bin +0 -0
  25. package/dist/browser-plane/preview-deferred/4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02.bin +0 -0
  26. package/dist/browser-plane/preview-deferred/521a0e528916d678d7588b2cddf3dadd3bb959a6d62426382f61c77a82017e35.bin +1 -0
  27. package/dist/browser-plane/preview-deferred/62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9.bin +0 -0
  28. package/dist/browser-plane/preview-deferred/642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e.bin +0 -0
  29. package/dist/browser-plane/preview-deferred/97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1.bin +0 -0
  30. package/dist/browser-plane/preview-deferred/a33cfe728c584fdba4fcc1fd54bcdc2f9f2f13889ddbb5b2bd1d0f8cbe49b84e.bin +28 -0
  31. package/dist/browser-plane/preview-deferred/bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa.bin +0 -0
  32. package/dist/browser-plane/preview-deferred/c3457ce58c96b65f303fec0ddf5f06733004eaf9e21e62302b155ec63a7765f7.bin +56 -0
  33. package/dist/browser-plane/preview-deferred/d7598e12c5dbef095ff8272cfc55da0250bd07fbdecbac8a530b9b277872a134.bin +0 -0
  34. package/dist/browser-plane/preview-deferred/dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b.bin +0 -0
  35. package/dist/browser-plane/preview-deferred/dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5.bin +0 -0
  36. package/dist/browser-plane/preview-deferred/ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf.bin +0 -0
  37. package/dist/browser-plane/sw.js +13 -0
  38. package/dist/browser-plane/t5-frame.html +350 -0
  39. package/dist/browser-plane/t5-worker-frame.html +350 -0
  40. package/dist/cli.mjs +6499 -5089
  41. package/dist/preview-assets.generated.bin +0 -0
  42. package/fixtures/wix-canned.json +34 -1
  43. package/guides/getting-started.md +2 -1
  44. package/guides/himi-json-output.md +8 -0
  45. package/guides/himi-skill.md +2 -1
  46. package/guides/iap-screen-adoption.md +4 -4
  47. package/guides/importable-sdks.md +1 -1
  48. package/guides/one-app-section-mappings.md +3 -3
  49. package/guides/one-app-to-himi-app.md +81 -5
  50. package/guides/sdk-reference.json +8862 -4388
  51. package/guides/skill.md +11 -1
  52. package/package.json +3 -3
  53. package/templates/beauty-salon/tier5-src/_shared/branded-lite-hosted.ts +1 -0
  54. package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +1 -1
  55. package/templates/branded-lite/config.ts +7 -0
  56. package/templates/branded-lite/dev/index.ts +3340 -1890
  57. package/templates/branded-lite/himalaya.content.json +3 -0
  58. package/templates/branded-lite/l10n/en.json +8 -4
  59. package/templates/branded-lite/tier5-src/_shared/branded-lite-hosted.ts +1 -0
  60. package/templates/branded-lite/tier5-src/_shared/business-chat-history.ts +4 -1
  61. package/templates/branded-lite/tier5-src/_shared/business-chat-presenter.ts +2 -7
  62. package/templates/branded-lite/tier5-src/_shared/header-badges.ts +1 -1
  63. package/templates/branded-lite/tier5-src/_shared/inbox-rooms.ts +68 -0
  64. package/templates/branded-lite/tier5-src/_shared/loyalty-offerings.ts +73 -0
  65. package/templates/branded-lite/tier5-src/_shared/member-activity.ts +34 -0
  66. package/templates/branded-lite/tier5-src/_shared/member-row-extras.ts +167 -0
  67. package/templates/branded-lite/tier5-src/_shared/notification-target.ts +141 -0
  68. package/templates/branded-lite/tier5-src/_shared/source-badges.ts +6 -4
  69. package/templates/branded-lite/tier5-src/_shared/wix-member-links.ts +114 -0
  70. package/templates/branded-lite/tier5-src/business-chat/index.ts +114 -59
  71. package/templates/branded-lite/tier5-src/himi-branded-lite-inbox/index.ts +83 -0
  72. package/templates/branded-lite/tier5-src/himi-branded-lite-loyalty/earning.ts +148 -0
  73. package/templates/branded-lite/tier5-src/himi-branded-lite-loyalty/index.ts +57 -31
  74. package/templates/branded-lite/tier5-src/himi-branded-lite-loyalty-rewards/index.ts +208 -0
  75. package/templates/branded-lite/tier5-src/himi-branded-lite-notifications/index.ts +62 -7
  76. package/templates/branded-lite/tier5-src/himi-store-product/index.ts +1 -1
  77. package/templates/branded-lite/tier5-src/wix-spaces-members/index.ts +60 -12
  78. package/templates/community-club/tier5-src/_shared/branded-lite-hosted.ts +1 -0
  79. package/templates/events-meetups/tier5-src/_shared/branded-lite-hosted.ts +1 -0
  80. package/templates/gym-fitness/tier5-src/_shared/branded-lite-hosted.ts +1 -0
  81. package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +1 -1
  82. package/templates/nonprofit-cause/tier5-src/_shared/branded-lite-hosted.ts +1 -0
  83. package/templates/podcast-creator/tier5-src/_shared/branded-lite-hosted.ts +1 -0
  84. package/templates/portfolio-creative/tier5-src/_shared/branded-lite-hosted.ts +1 -0
  85. package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +1 -1
  86. package/templates/restaurant/tier5-src/_shared/branded-lite-hosted.ts +1 -0
  87. package/templates/retail-store/tier5-src/_shared/branded-lite-hosted.ts +1 -0
  88. package/templates/retail-store/tier5-src/himi-store-product/index.ts +1 -1
  89. package/templates/salon-spa/dev/index.ts +112 -70
  90. package/templates/salon-spa/tier5-src/_shared/branded-lite-hosted.ts +1 -0
  91. package/templates/salon-spa/tier5-src/_shared/business-chat-history.ts +4 -1
  92. package/templates/salon-spa/tier5-src/_shared/business-chat-presenter.ts +2 -7
  93. package/templates/salon-spa/tier5-src/_shared/inbox-rooms.ts +68 -0
  94. package/templates/salon-spa/tier5-src/business-chat/index.ts +114 -59
  95. package/templates/yoga-studio/tier5-src/_shared/branded-lite-hosted.ts +1 -0
@@ -10,6 +10,10 @@ import { defineService as registerService } from "../_shared/collection-service.
10
10
  import { LOADING, loaded, loadFailed, type LoadCopy, type LoadFlags } from "../_shared/branded-load-state.js";
11
11
  import { queryNotifications, markRead, markAllRead, type MfvNotification } from "@himalaya/wix-members-feed-viewer";
12
12
  import { assertMemberAccess, memberAccess, type MemberAccess } from "../_shared/member-access.js";
13
+ import { navigate } from "@himalaya/state";
14
+ import { notificationOpen, urlHost, type NotificationOpen } from "../_shared/notification-target.js";
15
+ import { openRichLink } from "../_shared/rich-content-link.js";
16
+ import { copyParam, copyTemplate } from "../_shared/himi-copy.js";
13
17
 
14
18
  interface Row {
15
19
  id: string;
@@ -22,9 +26,13 @@ interface Row {
22
26
  photo: string;
23
27
  initial: string;
24
28
  dest: string;
29
+ /** What a tap does (a screen, a web link or nothing). Absent on the preview samples, which open nothing. */
30
+ open?: NotificationOpen;
25
31
  }
26
32
  interface S extends LoadFlags {
27
33
  items: Row[]; emptyText: string; actionMessage: string;
34
+ /** The web link the last tap named and is waiting for a second tap to open (see `open`). */
35
+ pendingUrl: string;
28
36
  /** Ownership travels with the rows, including a state captured by an async write. */
29
37
  inboxMember: MemberAccess | null;
30
38
  isMock: boolean; notMock: boolean;
@@ -67,7 +75,17 @@ function photo(n: MfvNotification): string {
67
75
  return n.icon?.personAvatarData?.data || n.icon?.webUrlData?.webUrl || "";
68
76
  }
69
77
 
70
- function norm(n: MfvNotification): Row {
78
+ /** The screens the Notifications screen says its app serves, or undefined when it did not say. */
79
+ function servedScreens(arg: unknown): ReadonlySet<string> | undefined {
80
+ const served = (arg as { served?: unknown } | null | undefined)?.served;
81
+ // A Retry reaches `load` with no params (the collection layer re-dispatches the action bare), so the list the screen
82
+ // last named is kept: dropping it would let every catalog link be tried again.
83
+ if (typeof served === "string") lastServed = new Set(served.split(",").filter(Boolean));
84
+ return lastServed;
85
+ }
86
+ let lastServed: ReadonlySet<string> | undefined;
87
+
88
+ function norm(n: MfvNotification, served?: ReadonlySet<string>): Row {
71
89
  const name = (n.title || n.message || "Notification").trim() || "Notification";
72
90
  const rel = relativeTime(n.sentDate);
73
91
  const lead = n.title && n.message ? n.message.trim() : "";
@@ -83,6 +101,7 @@ function norm(n: MfvNotification): Row {
83
101
  photo: p,
84
102
  initial: (name[0] || "#").toUpperCase(),
85
103
  dest: dest(n),
104
+ open: notificationOpen(n.deeplink, served),
86
105
  };
87
106
  }
88
107
 
@@ -128,12 +147,13 @@ function currentInbox(s: S): S | undefined {
128
147
  }
129
148
 
130
149
  const defineService = withOnlineWrites(registerService, ["markOneRead","markAll"], { messageFields: ["actionMessage"] });
131
- const INITIAL = { ...LOADING, items: [] as Row[], emptyText: "", actionMessage: "", inboxMember: null };
150
+ const INITIAL = { ...LOADING, items: [] as Row[], emptyText: "", actionMessage: "", pendingUrl: "", inboxMember: null };
132
151
  const svc = defineService<S, {
133
152
  prepare: (s: S) => S;
134
- load: (s: S) => Promise<S>;
153
+ load: (s: S, arg?: unknown) => Promise<S>;
135
154
  markOneRead: (s: S, id: string) => Promise<S | undefined>;
136
155
  markAll: (s: S) => Promise<S | undefined>;
156
+ open: (s: S, arg?: unknown) => Promise<S | undefined>;
137
157
  }>("himiBrandedNotifications", {
138
158
  state: INITIAL,
139
159
  actions: {
@@ -141,14 +161,16 @@ const svc = defineService<S, {
141
161
  // Native hosts may already cold-paint their account-scoped snapshot; this
142
162
  // reducer does not control that earlier host phase.
143
163
  prepare: (s) => ({ ...s, ...INITIAL }),
144
- load: async (s) => {
164
+ load: async (s, arg) => {
165
+ // The screen names which of the link targets this app serves (a comma list); none given means any may be tried.
166
+ const served = servedScreens(arg);
145
167
  try {
146
168
  const member = await requireMember();
147
- const items = (await queryNotifications(25)).map(norm);
169
+ const items = (await queryNotifications(25)).map((n) => norm(n, served));
148
170
  await assertMemberAccess(member);
149
- return { ...s, ...loaded("items", items, COPY), inboxMember: member, actionMessage: "" };
171
+ return { ...s, ...loaded("items", items, COPY), inboxMember: member, actionMessage: "", pendingUrl: "" };
150
172
  } catch (err) {
151
- return { ...s, ...loadFailed("items", (await memberAccess()).email ? err : signInRequired(), COPY), inboxMember: null };
173
+ return { ...s, ...loadFailed("items", (await memberAccess()).email ? err : signInRequired(), COPY), inboxMember: null, actionMessage: "", pendingUrl: "" };
152
174
  }
153
175
  },
154
176
  // Mark one read; optimistically clear its dot. Real write is best-effort and
@@ -174,6 +196,39 @@ const svc = defineService<S, {
174
196
  const current = currentInbox(s);
175
197
  return current ? { ...current, items: current.items.map((r) => (r.id === id ? { ...r, unread: false, unreadDot: "" } : r)) } : undefined;
176
198
  },
199
+ // A tap on a row opens what the notification points at, like the One App: a Himi screen when its link names
200
+ // one of ours (see _shared/notification-target.ts), else its https link, else a visible "can't open".
201
+ // A web link is written by the site, so it needs a SECOND tap on the same row, after the first tap has
202
+ // shown where it goes. It never replaces the inbox, and an unknown link never guesses a screen.
203
+ open: async (s, arg): Promise<S | undefined> => {
204
+ const id = String((arg as { id?: unknown } | null | undefined)?.id ?? "");
205
+ const row = s.items.find((r) => r.id === id);
206
+ if (!row || s.busy) return undefined;
207
+ if (!s.isMock) {
208
+ try { await requireInboxMember(s); } catch (err) {
209
+ const current = currentInbox(s);
210
+ return current ? { ...current, ...loadFailed("items", err, COPY), inboxMember: null } : undefined;
211
+ }
212
+ }
213
+ const current = currentInbox(s);
214
+ if (!current) return undefined;
215
+ const action = row.open ?? { kind: "none" as const };
216
+ if (action.kind === "url") {
217
+ if (current.pendingUrl !== action.url) {
218
+ return { ...current, pendingUrl: action.url, actionMessage: copyTemplate("notifications.openLinkText", "Tap again to open {host}.", { host: urlHost(action.url) }) };
219
+ }
220
+ const opened = await openRichLink(action.url);
221
+ const now = currentInbox(s);
222
+ return now ? { ...now, pendingUrl: "", actionMessage: opened ? "" : copyParam("notifications.cantOpenText", "This notification can't be opened here.") } : undefined;
223
+ }
224
+ if (action.kind === "none") {
225
+ return { ...current, pendingUrl: "", actionMessage: copyParam("notifications.cantOpenText", "This notification can't be opened here.") };
226
+ }
227
+ // Read on open: the dot clears at once, the write is best effort and never holds the navigation.
228
+ if (!id.startsWith("sample-") && row.unread) markRead([id]).catch(() => undefined);
229
+ const read = { ...current, pendingUrl: "", actionMessage: "", items: current.items.map((r) => (r.id === id ? { ...r, unread: false, unreadDot: "" } : r)) };
230
+ return navigate(read, { screenId: action.target.screenId, args: action.target.args });
231
+ },
177
232
  // Mark everything read; optimistically clear all dots.
178
233
  markAll: async (s): Promise<S | undefined> => {
179
234
  if (s.busy) return undefined;
@@ -439,7 +439,7 @@ function derive(s: S): S {
439
439
  let cartBadgeEpoch = 0;
440
440
 
441
441
  // A digital product with no options sells through the App Store once the platform sells Wix Stores
442
- // items there (`storesAppleSalesEnabled`), through StartItemPayment or the app's own eCom order.
442
+ // items there (`storesAppleSalesEnabled`): wix-iap StartItemPayment places and charges its order.
443
443
  // Until then, and for any other product, an App Store route only blocks the regular checkout.
444
444
  const SELLS_THROUGH_APPLE = storesAppleSalesEnabled();
445
445
  const WIX_STORES_APP = "215238eb-22a5-4c36-9e7b-e7c08025e04e";
@@ -1,23 +1,34 @@
1
1
  // Public site-member directory. Search, sorting, totals and offset paging match
2
2
  // OneApp's Members v1 query; no owner roster or build-time count is substituted.
3
3
  import { defineService } from "@himalaya/state";
4
- import { queryMembers, type WixMember } from "@himalaya/wix-members";
4
+ import { queryMembers, followMember, unfollowMember, type WixMember } from "@himalaya/wix-members";
5
5
  import { createCollectionQuery } from "../_shared/collection-query.js";
6
6
  import { readAccess, assertReadAccess, readAccessDenied, type ReadAccess } from "../_shared/member-read.js";
7
7
  import { memberAccessChanged } from "../_shared/member-access.js";
8
8
  import { withEmptyFlags } from "../_shared/empty-flags.js";
9
+ import { copyParam, stashCopy } from "../_shared/himi-copy.js";
10
+ import { memberRowExtras, followLabel, type MemberRowExtras } from "../_shared/member-row-extras.js";
9
11
 
10
- interface Row { id: string; name: string; subtitle: string; initial: string; photo: string; sectionTitle: string }
12
+ interface Row extends MemberRowExtras { id: string; name: string; initial: string; photo: string; sectionTitle: string }
11
13
  interface S {
12
14
  status: "loading" | "ready" | "error";
13
15
  members: Row[]; emptyText: string;
14
16
  query: string; total: number | null; searchLabel: string;
15
17
  nextCursor: string; noMore: boolean; noPageError: boolean;
16
18
  error: string; notErrored: boolean; notLoading: boolean; hideSearch: boolean;
19
+ followHint: string;
17
20
  }
18
21
  const PAGE_SIZE = 50;
19
22
  const SEARCH_FIELDS = ["contact.firstName", "contact.lastName", "profile.nickname"];
20
23
  let directoryAccess: ReadAccess | undefined;
24
+ // A Follow the signed-in member just made or undone. Rows keep the state they were read with, so this
25
+ // shows the change at once; the next first-page read replaces it with what the server says.
26
+ const followChanges = new Map<string, boolean>();
27
+ const pendingFollows = new Set<string>();
28
+
29
+ /** Who a directory read, and a write that follows it, belong to: the site and the signed-in member. A reload
30
+ * builds a NEW ReadAccess object for the same reader, so identity says nothing; this key does. */
31
+ const readerKey = (access: ReadAccess | undefined): string => access ? JSON.stringify([access.tenant, access.member.scope, access.member.email]) : "";
21
32
 
22
33
  function name(m: WixMember): string {
23
34
  const nick = m.profile?.nickname?.trim();
@@ -25,10 +36,14 @@ function name(m: WixMember): string {
25
36
  const full = [m.contact?.firstName, m.contact?.lastName].filter(Boolean).join(" ").trim();
26
37
  return full || "Member";
27
38
  }
28
- function row(m: WixMember): Row {
39
+ function row(m: WixMember, extras: MemberRowExtras): Row {
29
40
  const n = name(m);
30
41
  // Login emails are not directory subtitles. Only public profile content is presented.
31
- return { id: m.id, name: n, subtitle: "", initial: (n[0] || "#").toUpperCase(), photo: m.profile?.photo?.url || "", sectionTitle: "" };
42
+ return { id: m.id, name: n, initial: (n[0] || "#").toUpperCase(), photo: m.profile?.photo?.url || "", sectionTitle: "", ...extras };
43
+ }
44
+ function withFollowChange(item: Row): Row {
45
+ const following = followChanges.get(item.id);
46
+ return following === undefined ? item : { ...item, isFollowing: following, followLabel: followLabel(following) };
32
47
  }
33
48
  function sections(rows: Row[]): Row[] {
34
49
  const groups = new Map<string, Row[]>();
@@ -47,16 +62,17 @@ const directory = createCollectionQuery<Row>({
47
62
  if (!Number.isSafeInteger(offset) || offset < 0) throw new Error("Invalid member page offset");
48
63
  const access = cursor ? directoryAccess : await readAccess();
49
64
  if (!access) throw memberAccessChanged();
50
- if (!cursor) directoryAccess = access;
65
+ if (!cursor) { directoryAccess = access; followChanges.clear(); followHint = ""; }
51
66
  await assertReadAccess(access);
52
67
  const response = await queryMembers({ query: {
53
68
  filter: { status: "APPROVED", privacyStatus: "PUBLIC" },
54
69
  sorting: [{ fieldName: "profile.nickname", order: "ASC" }],
55
70
  paging: { limit: PAGE_SIZE, offset },
56
71
  }, ...(criteria.query ? { search: { expression: criteria.query, fields: SEARCH_FIELDS } } : {}) });
72
+ if (!Array.isArray(response.members) || response.members.some(member => !member?.id)) throw new Error("Invalid member page");
73
+ const extras = response.members.length ? await memberRowExtras(response.members.map(member => member.id), access) : undefined;
57
74
  await assertReadAccess(access);
58
75
  if (access !== directoryAccess) throw memberAccessChanged();
59
- if (!Array.isArray(response.members) || response.members.some(member => !member?.id)) throw new Error("Invalid member page");
60
76
  const metadata = response.metadata;
61
77
  if (metadata?.offset !== undefined && metadata.offset !== offset) throw new Error("Mismatched member page offset");
62
78
  const total = !metadata?.tooManyToCount && typeof metadata?.total === "number" && Number.isSafeInteger(metadata.total) && metadata.total >= 0
@@ -64,7 +80,7 @@ const directory = createCollectionQuery<Row>({
64
80
  const next = offset + response.members.length;
65
81
  if (!response.members.length && total !== undefined && total > next) throw new Error("Incomplete member page");
66
82
  return {
67
- items: response.members.map(row), total,
83
+ items: response.members.map(member => row(member, extras!(member.id))), total,
68
84
  nextCursor: response.members.length && (total === undefined ? response.members.length === PAGE_SIZE : next < total) ? String(next) : "",
69
85
  };
70
86
  },
@@ -73,6 +89,7 @@ const directory = createCollectionQuery<Row>({
73
89
  // Set once the reader has typed a search: from then on the field stays mounted, even while a cleared search
74
90
  // reloads the full directory (no rows, no query yet), so it never unmounts and loses focus mid-edit.
75
91
  let searched = false;
92
+ let followHint = "";
76
93
 
77
94
  function project(): S {
78
95
  const page = directory.getState();
@@ -81,7 +98,7 @@ function project(): S {
81
98
  const failed = page.status === "error";
82
99
  return {
83
100
  status: failed ? "error" : loading ? "loading" : "ready",
84
- members: sections(page.items), query: page.criteria.query, total,
101
+ members: sections(page.items.map(withFollowChange)), query: page.criteria.query, total,
85
102
  searchLabel: total === null ? "" : `${total} ${total === 1 ? "member" : "members"}`,
86
103
  emptyText: page.status === "ready" && !page.items.length ? page.criteria.query ? "No results" : "No members yet" : "",
87
104
  nextCursor: loading || page.loadingMore || page.pageError ? "" : page.nextCursor,
@@ -94,21 +111,24 @@ function project(): S {
94
111
  // A successful empty SEARCH keeps its field reachable (to clear the query); a directory that is
95
112
  // loading or truly empty (no query, no rows) has nothing to search.
96
113
  hideSearch: failed || (!page.items.length && !page.criteria.query && !searched),
114
+ followHint,
97
115
  };
98
116
  }
99
117
  const svc = defineService<S, {
100
- load: (s: S) => Promise<undefined>;
118
+ load: (s: S, arg?: unknown) => Promise<undefined>;
101
119
  filter: (s: S, arg?: unknown) => Promise<undefined>;
102
120
  loadMore: (s: S) => Promise<undefined>;
103
121
  retry: (s: S) => Promise<undefined>;
122
+ toggleFollow: (s: S, arg?: unknown) => Promise<S | undefined>;
104
123
  changed: (s: S) => S;
105
124
  }>("spacesMembers", withEmptyFlags({
106
125
  state: { status: "loading", members: [], emptyText: "", query: "", total: null, searchLabel: "", nextCursor: "", noMore: true, noPageError: true,
107
- error: "", notErrored: true, notLoading: false, hideSearch: true },
126
+ error: "", notErrored: true, notLoading: false, hideSearch: true, followHint: "" },
108
127
  // A restored worker must re-read the current reader/site before showing a roster.
109
- notHydrated: ["status", "members", "emptyText", "query", "total", "searchLabel", "nextCursor", "noMore", "noPageError", "error", "notErrored", "notLoading", "hideSearch"],
128
+ notHydrated: ["status", "members", "emptyText", "query", "total", "searchLabel", "nextCursor", "noMore", "noPageError", "error", "notErrored", "notLoading", "hideSearch", "followHint"],
110
129
  actions: {
111
- load: async () => { await directory.query(directory.getState().criteria); return undefined; },
130
+ // The screen's onAppear carries the copy pack (the owner's wording, the stock translations) and the app language.
131
+ load: async (_s, arg) => { stashCopy(arg); await directory.query(directory.getState().criteria); return undefined; },
112
132
  filter: async (_s, arg) => {
113
133
  const value = typeof arg === "string" ? arg : (arg as { value?: unknown })?.value;
114
134
  if (typeof value === "string" && value.trim()) searched = true;
@@ -116,6 +136,34 @@ const svc = defineService<S, {
116
136
  },
117
137
  loadMore: async () => { await directory.loadMore(); return undefined; },
118
138
  retry: async () => { await directory.retry(); return undefined; },
139
+ // Only a signed-in member reaches this: a Visitor's Follow opens sign-in from the descriptor instead.
140
+ toggleFollow: async (_s, arg) => {
141
+ const id = (arg as { id?: unknown } | undefined)?.id;
142
+ const access = directoryAccess;
143
+ if (typeof id !== "string" || !id || !access?.member.email || pendingFollows.has(id)) return undefined;
144
+ const row = directory.getState().items.find(item => item.id === id);
145
+ if (!row) return undefined;
146
+ const following = withFollowChange(row).isFollowing;
147
+ pendingFollows.add(id);
148
+ try {
149
+ await assertReadAccess(access);
150
+ if (following) await unfollowMember(id); else await followMember(id);
151
+ await assertReadAccess(access);
152
+ // A first-page reload (typing in search, Retry) may have replaced directoryAccess while the write was in
153
+ // flight. The write still stands for the same reader on the same site, so it is recorded; only another
154
+ // reader's list must not take it.
155
+ if (readerKey(access) !== readerKey(directoryAccess)) return undefined;
156
+ followChanges.set(id, !following);
157
+ followHint = "";
158
+ } catch {
159
+ // A failed write belongs to the member who tapped: the control keeps its state and says so.
160
+ if (readerKey(access) !== readerKey(directoryAccess)) return undefined;
161
+ followHint = copyParam("members.followError", "Couldn't update Follow — try again.");
162
+ } finally {
163
+ pendingFollows.delete(id);
164
+ }
165
+ return project();
166
+ },
119
167
  changed: () => project(),
120
168
  },
121
169
  }, { members: "noMembers" }));
@@ -97,6 +97,7 @@ export const HOSTED_SCREEN_VERTICALS: Readonly<Record<string, readonly BrandedLi
97
97
  himi_gift_card: ["giftCards"],
98
98
  himi_gift_cards: ["giftCards"],
99
99
  himi_groups: ["groups"],
100
+ himi_inbox: ["inbox"],
100
101
  himi_group_about: ["groups"],
101
102
  himi_my_groups: ["groups"],
102
103
  himi_invoice: ["invoices"],
@@ -97,6 +97,7 @@ export const HOSTED_SCREEN_VERTICALS: Readonly<Record<string, readonly BrandedLi
97
97
  himi_gift_card: ["giftCards"],
98
98
  himi_gift_cards: ["giftCards"],
99
99
  himi_groups: ["groups"],
100
+ himi_inbox: ["inbox"],
100
101
  himi_group_about: ["groups"],
101
102
  himi_my_groups: ["groups"],
102
103
  himi_invoice: ["invoices"],
@@ -97,6 +97,7 @@ export const HOSTED_SCREEN_VERTICALS: Readonly<Record<string, readonly BrandedLi
97
97
  himi_gift_card: ["giftCards"],
98
98
  himi_gift_cards: ["giftCards"],
99
99
  himi_groups: ["groups"],
100
+ himi_inbox: ["inbox"],
100
101
  himi_group_about: ["groups"],
101
102
  himi_my_groups: ["groups"],
102
103
  himi_invoice: ["invoices"],
@@ -467,7 +467,7 @@ function derive(s: S): S {
467
467
  let cartBadgeEpoch = 0;
468
468
 
469
469
  // A digital product with no options sells through the App Store once the platform sells Wix Stores
470
- // items there (`storesAppleSalesEnabled`), through StartItemPayment or the app's own eCom order.
470
+ // items there (`storesAppleSalesEnabled`): wix-iap StartItemPayment places and charges its order.
471
471
  // Until then, and for any other product, an App Store route only blocks the regular checkout.
472
472
  const SELLS_THROUGH_APPLE = storesAppleSalesEnabled();
473
473
  const WIX_STORES_APP = "215238eb-22a5-4c36-9e7b-e7c08025e04e";
@@ -97,6 +97,7 @@ export const HOSTED_SCREEN_VERTICALS: Readonly<Record<string, readonly BrandedLi
97
97
  himi_gift_card: ["giftCards"],
98
98
  himi_gift_cards: ["giftCards"],
99
99
  himi_groups: ["groups"],
100
+ himi_inbox: ["inbox"],
100
101
  himi_group_about: ["groups"],
101
102
  himi_my_groups: ["groups"],
102
103
  himi_invoice: ["invoices"],
@@ -97,6 +97,7 @@ export const HOSTED_SCREEN_VERTICALS: Readonly<Record<string, readonly BrandedLi
97
97
  himi_gift_card: ["giftCards"],
98
98
  himi_gift_cards: ["giftCards"],
99
99
  himi_groups: ["groups"],
100
+ himi_inbox: ["inbox"],
100
101
  himi_group_about: ["groups"],
101
102
  himi_my_groups: ["groups"],
102
103
  himi_invoice: ["invoices"],
@@ -97,6 +97,7 @@ export const HOSTED_SCREEN_VERTICALS: Readonly<Record<string, readonly BrandedLi
97
97
  himi_gift_card: ["giftCards"],
98
98
  himi_gift_cards: ["giftCards"],
99
99
  himi_groups: ["groups"],
100
+ himi_inbox: ["inbox"],
100
101
  himi_group_about: ["groups"],
101
102
  himi_my_groups: ["groups"],
102
103
  himi_invoice: ["invoices"],
@@ -467,7 +467,7 @@ function derive(s: S): S {
467
467
  let cartBadgeEpoch = 0;
468
468
 
469
469
  // A digital product with no options sells through the App Store once the platform sells Wix Stores
470
- // items there (`storesAppleSalesEnabled`), through StartItemPayment or the app's own eCom order.
470
+ // items there (`storesAppleSalesEnabled`): wix-iap StartItemPayment places and charges its order.
471
471
  // Until then, and for any other product, an App Store route only blocks the regular checkout.
472
472
  const SELLS_THROUGH_APPLE = storesAppleSalesEnabled();
473
473
  const WIX_STORES_APP = "215238eb-22a5-4c36-9e7b-e7c08025e04e";
@@ -97,6 +97,7 @@ export const HOSTED_SCREEN_VERTICALS: Readonly<Record<string, readonly BrandedLi
97
97
  himi_gift_card: ["giftCards"],
98
98
  himi_gift_cards: ["giftCards"],
99
99
  himi_groups: ["groups"],
100
+ himi_inbox: ["inbox"],
100
101
  himi_group_about: ["groups"],
101
102
  himi_my_groups: ["groups"],
102
103
  himi_invoice: ["invoices"],
@@ -97,6 +97,7 @@ export const HOSTED_SCREEN_VERTICALS: Readonly<Record<string, readonly BrandedLi
97
97
  himi_gift_card: ["giftCards"],
98
98
  himi_gift_cards: ["giftCards"],
99
99
  himi_groups: ["groups"],
100
+ himi_inbox: ["inbox"],
100
101
  himi_group_about: ["groups"],
101
102
  himi_my_groups: ["groups"],
102
103
  himi_invoice: ["invoices"],
@@ -439,7 +439,7 @@ function derive(s: S): S {
439
439
  let cartBadgeEpoch = 0;
440
440
 
441
441
  // A digital product with no options sells through the App Store once the platform sells Wix Stores
442
- // items there (`storesAppleSalesEnabled`), through StartItemPayment or the app's own eCom order.
442
+ // items there (`storesAppleSalesEnabled`): wix-iap StartItemPayment places and charges its order.
443
443
  // Until then, and for any other product, an App Store route only blocks the regular checkout.
444
444
  const SELLS_THROUGH_APPLE = storesAppleSalesEnabled();
445
445
  const WIX_STORES_APP = "215238eb-22a5-4c36-9e7b-e7c08025e04e";