@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.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments-contract.d.ts +4 -14
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments-wire.d.ts +2 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments.d.ts +1 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/apple-iap-contract.d.ts +26 -13
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/apple-iap-core.d.ts +0 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/checkout-source.d.ts +3 -10
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/commerce-core.d.ts +0 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/iap-order-backend.d.ts +0 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/iap-verticals/stores.d.ts +2 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/index.d.ts +2 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/loyalty/src/index.d.ts +64 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/members-feed-viewer/src/index.d.ts +10 -6
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/members-v1/src/index.d.ts +36 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +13 -45
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +2 -4
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +7 -11
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-loyalty.mjs +39 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-members-feed-viewer.mjs +8 -7
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-members.mjs +29 -0
- package/dist/browser-plane/assets.json +166 -0
- package/dist/browser-plane/host.html +156 -0
- package/dist/browser-plane/package-reader.js +4 -0
- package/dist/browser-plane/preview-deferred/368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5.bin +0 -0
- package/dist/browser-plane/preview-deferred/37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29.bin +0 -0
- package/dist/browser-plane/preview-deferred/4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02.bin +0 -0
- package/dist/browser-plane/preview-deferred/521a0e528916d678d7588b2cddf3dadd3bb959a6d62426382f61c77a82017e35.bin +1 -0
- package/dist/browser-plane/preview-deferred/62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9.bin +0 -0
- package/dist/browser-plane/preview-deferred/642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e.bin +0 -0
- package/dist/browser-plane/preview-deferred/97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1.bin +0 -0
- package/dist/browser-plane/preview-deferred/a33cfe728c584fdba4fcc1fd54bcdc2f9f2f13889ddbb5b2bd1d0f8cbe49b84e.bin +28 -0
- package/dist/browser-plane/preview-deferred/bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa.bin +0 -0
- package/dist/browser-plane/preview-deferred/c3457ce58c96b65f303fec0ddf5f06733004eaf9e21e62302b155ec63a7765f7.bin +56 -0
- package/dist/browser-plane/preview-deferred/d7598e12c5dbef095ff8272cfc55da0250bd07fbdecbac8a530b9b277872a134.bin +0 -0
- package/dist/browser-plane/preview-deferred/dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b.bin +0 -0
- package/dist/browser-plane/preview-deferred/dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5.bin +0 -0
- package/dist/browser-plane/preview-deferred/ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf.bin +0 -0
- package/dist/browser-plane/sw.js +13 -0
- package/dist/browser-plane/t5-frame.html +350 -0
- package/dist/browser-plane/t5-worker-frame.html +350 -0
- package/dist/cli.mjs +6499 -5089
- package/dist/preview-assets.generated.bin +0 -0
- package/fixtures/wix-canned.json +34 -1
- package/guides/getting-started.md +2 -1
- package/guides/himi-json-output.md +8 -0
- package/guides/himi-skill.md +2 -1
- package/guides/iap-screen-adoption.md +4 -4
- package/guides/importable-sdks.md +1 -1
- package/guides/one-app-section-mappings.md +3 -3
- package/guides/one-app-to-himi-app.md +81 -5
- package/guides/sdk-reference.json +8862 -4388
- package/guides/skill.md +11 -1
- package/package.json +3 -3
- package/templates/beauty-salon/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/beauty-salon/tier5-src/himi-store-product/index.ts +1 -1
- package/templates/branded-lite/config.ts +7 -0
- package/templates/branded-lite/dev/index.ts +3340 -1890
- package/templates/branded-lite/himalaya.content.json +3 -0
- package/templates/branded-lite/l10n/en.json +8 -4
- package/templates/branded-lite/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/branded-lite/tier5-src/_shared/business-chat-history.ts +4 -1
- package/templates/branded-lite/tier5-src/_shared/business-chat-presenter.ts +2 -7
- package/templates/branded-lite/tier5-src/_shared/header-badges.ts +1 -1
- package/templates/branded-lite/tier5-src/_shared/inbox-rooms.ts +68 -0
- package/templates/branded-lite/tier5-src/_shared/loyalty-offerings.ts +73 -0
- package/templates/branded-lite/tier5-src/_shared/member-activity.ts +34 -0
- package/templates/branded-lite/tier5-src/_shared/member-row-extras.ts +167 -0
- package/templates/branded-lite/tier5-src/_shared/notification-target.ts +141 -0
- package/templates/branded-lite/tier5-src/_shared/source-badges.ts +6 -4
- package/templates/branded-lite/tier5-src/_shared/wix-member-links.ts +114 -0
- package/templates/branded-lite/tier5-src/business-chat/index.ts +114 -59
- package/templates/branded-lite/tier5-src/himi-branded-lite-inbox/index.ts +83 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-loyalty/earning.ts +148 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-loyalty/index.ts +57 -31
- package/templates/branded-lite/tier5-src/himi-branded-lite-loyalty-rewards/index.ts +208 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-notifications/index.ts +62 -7
- package/templates/branded-lite/tier5-src/himi-store-product/index.ts +1 -1
- package/templates/branded-lite/tier5-src/wix-spaces-members/index.ts +60 -12
- package/templates/community-club/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/events-meetups/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/gym-fitness/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/gym-fitness/tier5-src/himi-store-product/index.ts +1 -1
- package/templates/nonprofit-cause/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/podcast-creator/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/portfolio-creative/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/portfolio-creative/tier5-src/himi-store-product/index.ts +1 -1
- package/templates/restaurant/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/retail-store/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/retail-store/tier5-src/himi-store-product/index.ts +1 -1
- package/templates/salon-spa/dev/index.ts +112 -70
- package/templates/salon-spa/tier5-src/_shared/branded-lite-hosted.ts +1 -0
- package/templates/salon-spa/tier5-src/_shared/business-chat-history.ts +4 -1
- package/templates/salon-spa/tier5-src/_shared/business-chat-presenter.ts +2 -7
- package/templates/salon-spa/tier5-src/_shared/inbox-rooms.ts +68 -0
- package/templates/salon-spa/tier5-src/business-chat/index.ts +114 -59
- 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
|
-
|
|
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`)
|
|
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;
|
|
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,
|
|
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
|
-
|
|
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`)
|
|
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`)
|
|
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`)
|
|
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";
|