@wix/himalaya-cli 0.1404.0 → 0.1406.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/dev-server/src/app-config.d.ts +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/app-module.d.ts +3 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/builder.d.ts +3 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +88 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/validator.d.ts +350 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/ai-site-chat/src/extensions.d.ts +10 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/ai-site-chat/src/index.d.ts +102 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +82 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/sdui.mjs +83 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-ai-site-chat.mjs +70 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/package.json +4 -0
- package/dist/cli.mjs +250 -40
- package/dist/preview-assets.generated.bin +0 -0
- package/fixtures/wix-canned.json +3 -0
- package/guides/component-reference.json +3 -2
- package/guides/component-reference.md +7 -1
- package/guides/importable-sdks.md +1 -0
- package/guides/sdk-reference.json +133 -2
- package/package.json +2 -2
- package/templates/beauty-salon/config.ts +7 -0
- package/templates/beauty-salon/dev/index.ts +1750 -0
- package/templates/beauty-salon/himalaya.content.json +3 -0
- package/templates/beauty-salon/site-functions/README.md +37 -0
- package/templates/beauty-salon/site-functions/himiMemberPhotoUploadUrl.js +90 -0
- package/templates/beauty-salon/tier5-src/_shared/member-account-routes.ts +11 -3
- package/templates/beauty-salon/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
- package/templates/beauty-salon/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
- package/templates/branded-lite/config.ts +7 -0
- package/templates/branded-lite/dev/index.ts +1750 -0
- package/templates/branded-lite/himalaya.content.json +3 -0
- package/templates/branded-lite/site-functions/README.md +37 -0
- package/templates/branded-lite/site-functions/himiMemberPhotoUploadUrl.js +90 -0
- package/templates/branded-lite/tier5-src/_shared/member-account-routes.ts +11 -3
- package/templates/branded-lite/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
- package/templates/community-club/config.ts +7 -0
- package/templates/community-club/dev/index.ts +1750 -0
- package/templates/community-club/himalaya.content.json +3 -0
- package/templates/community-club/site-functions/README.md +37 -0
- package/templates/community-club/site-functions/himiMemberPhotoUploadUrl.js +90 -0
- package/templates/community-club/tier5-src/_shared/member-account-routes.ts +11 -3
- package/templates/community-club/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
- package/templates/community-club/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
- package/templates/events-meetups/config.ts +7 -0
- package/templates/events-meetups/dev/index.ts +1750 -0
- package/templates/events-meetups/himalaya.content.json +3 -0
- package/templates/events-meetups/site-functions/README.md +37 -0
- package/templates/events-meetups/site-functions/himiMemberPhotoUploadUrl.js +90 -0
- package/templates/events-meetups/tier5-src/_shared/member-account-routes.ts +11 -3
- package/templates/events-meetups/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
- package/templates/events-meetups/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
- package/templates/gym-fitness/config.ts +7 -0
- package/templates/gym-fitness/dev/index.ts +1750 -0
- package/templates/gym-fitness/himalaya.content.json +3 -0
- package/templates/gym-fitness/site-functions/README.md +37 -0
- package/templates/gym-fitness/site-functions/himiMemberPhotoUploadUrl.js +90 -0
- package/templates/gym-fitness/tier5-src/_shared/member-account-routes.ts +11 -3
- package/templates/gym-fitness/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
- package/templates/gym-fitness/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
- package/templates/nonprofit-cause/config.ts +7 -0
- package/templates/nonprofit-cause/dev/index.ts +1750 -0
- package/templates/nonprofit-cause/himalaya.content.json +3 -0
- package/templates/nonprofit-cause/site-functions/README.md +37 -0
- package/templates/nonprofit-cause/site-functions/himiMemberPhotoUploadUrl.js +90 -0
- package/templates/nonprofit-cause/tier5-src/_shared/member-account-routes.ts +11 -3
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
- package/templates/podcast-creator/config.ts +7 -0
- package/templates/podcast-creator/dev/index.ts +1750 -0
- package/templates/podcast-creator/himalaya.content.json +3 -0
- package/templates/podcast-creator/site-functions/README.md +37 -0
- package/templates/podcast-creator/site-functions/himiMemberPhotoUploadUrl.js +90 -0
- package/templates/podcast-creator/tier5-src/_shared/member-account-routes.ts +11 -3
- package/templates/podcast-creator/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
- package/templates/podcast-creator/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
- package/templates/portfolio-creative/config.ts +7 -0
- package/templates/portfolio-creative/dev/index.ts +1750 -0
- package/templates/portfolio-creative/himalaya.content.json +3 -0
- package/templates/portfolio-creative/site-functions/README.md +37 -0
- package/templates/portfolio-creative/site-functions/himiMemberPhotoUploadUrl.js +90 -0
- package/templates/portfolio-creative/tier5-src/_shared/member-account-routes.ts +11 -3
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
- package/templates/restaurant/config.ts +7 -0
- package/templates/restaurant/dev/index.ts +1750 -0
- package/templates/restaurant/himalaya.content.json +3 -0
- package/templates/restaurant/site-functions/README.md +37 -0
- package/templates/restaurant/site-functions/himiMemberPhotoUploadUrl.js +90 -0
- package/templates/restaurant/tier5-src/_shared/member-account-routes.ts +11 -3
- package/templates/restaurant/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
- package/templates/restaurant/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
- package/templates/retail-store/config.ts +7 -0
- package/templates/retail-store/dev/index.ts +1750 -0
- package/templates/retail-store/himalaya.content.json +3 -0
- package/templates/retail-store/site-functions/README.md +37 -0
- package/templates/retail-store/site-functions/himiMemberPhotoUploadUrl.js +90 -0
- package/templates/retail-store/tier5-src/_shared/member-account-routes.ts +11 -3
- package/templates/retail-store/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
- package/templates/retail-store/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
- package/templates/yoga-studio/config.ts +7 -0
- package/templates/yoga-studio/dev/index.ts +1750 -0
- package/templates/yoga-studio/himalaya.content.json +3 -0
- package/templates/yoga-studio/site-functions/README.md +37 -0
- package/templates/yoga-studio/site-functions/himiMemberPhotoUploadUrl.js +90 -0
- package/templates/yoga-studio/tier5-src/_shared/member-account-routes.ts +11 -3
- package/templates/yoga-studio/tier5-src/himi-branded-lite-account-settings/blocked-members.ts +242 -0
- package/templates/yoga-studio/tier5-src/himi-branded-lite-account-settings/index.ts +1091 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# My Account profile photo upload URL
|
|
2
|
+
|
|
3
|
+
`himiMemberPhotoUploadUrl.js` is a Wix Velo HTTP function for minting a single
|
|
4
|
+
public Media Manager upload URL for the signed-in member's profile photo. It
|
|
5
|
+
requires the request to carry the member's Wix session through the authenticated
|
|
6
|
+
REST invoke path. A direct anonymous `/_functions` request is not sufficient.
|
|
7
|
+
|
|
8
|
+
The function reads the member identity with `currentMember.getMember()` before
|
|
9
|
+
elevating only `files.generateFileUploadUrl()`. It accepts `mimeType` and
|
|
10
|
+
`fileName`; the filename is generated on the server. It rejects caller-provided
|
|
11
|
+
member, site, host, or folder values and allows JPEG, PNG, and WebP only. The
|
|
12
|
+
generated upload is public because it is intended to be displayed as the member's
|
|
13
|
+
profile photo. The upload URL is a bearer capability: keep it transient and do
|
|
14
|
+
not log or persist it.
|
|
15
|
+
|
|
16
|
+
The upload API does not expose a maximum byte-size option when minting this URL;
|
|
17
|
+
the site function therefore cannot enforce the eventual PUT body size. The
|
|
18
|
+
current native MediaField uploads directly to the signed URL and does not apply
|
|
19
|
+
the `presigned_upload` capability's byte limit. Do not claim a size guarantee or
|
|
20
|
+
add an invented size parameter to the Wix call.
|
|
21
|
+
|
|
22
|
+
To install this function on a customer site, the site owner must publish it:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
himi site function put himiMemberPhotoUploadUrl --method POST --source <file> --site <id> --url <site> --publish
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Publishing makes the latest saved site revision live, which can include other
|
|
29
|
+
unpublished site changes. Review that revision before publishing. This source
|
|
30
|
+
change does not publish or invoke a live site.
|
|
31
|
+
|
|
32
|
+
After installing the app, opt in by setting
|
|
33
|
+
`HIMI_BRANDED_LITE_MEMBER_PHOTO_UPLOAD_FUNCTION=himiMemberPhotoUploadUrl` in the
|
|
34
|
+
app runtime environment. The generated template reads this value at runtime;
|
|
35
|
+
template generation replaces its local value with a placeholder so the
|
|
36
|
+
generator machine's configuration is never baked into customer templates.
|
|
37
|
+
Without the opt-in, the profile photo picker stays hidden.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Member-authenticated upload URL mint for My Account profile photos.
|
|
2
|
+
// This function is merged into a Wix site's backend/http-functions.js by
|
|
3
|
+
// `himi site function put`; keep it self-contained so the CLI can preserve it.
|
|
4
|
+
import { badRequest, forbidden, ok, serverError } from "wix-http-functions";
|
|
5
|
+
import { elevate } from "wix-auth";
|
|
6
|
+
import { files } from "wix-media.v2";
|
|
7
|
+
import { currentMember } from "wix-members-backend";
|
|
8
|
+
|
|
9
|
+
export async function post_himiMemberPhotoUploadUrl(request) {
|
|
10
|
+
const response = (builder, body) => builder({
|
|
11
|
+
headers: { "Content-Type": "application/json" },
|
|
12
|
+
body,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
let input;
|
|
16
|
+
try {
|
|
17
|
+
input = await request.body.json();
|
|
18
|
+
} catch {
|
|
19
|
+
return response(badRequest, { error: "invalid_request" });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
23
|
+
return response(badRequest, { error: "invalid_request" });
|
|
24
|
+
}
|
|
25
|
+
const suppliedKeys = Object.keys(input);
|
|
26
|
+
if (suppliedKeys.some((key) => key !== "mimeType" && key !== "fileName")) {
|
|
27
|
+
return response(badRequest, { error: "invalid_request" });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const extensionByMimeType = Object.freeze({
|
|
31
|
+
"image/jpeg": "jpg",
|
|
32
|
+
"image/png": "png",
|
|
33
|
+
"image/webp": "webp",
|
|
34
|
+
});
|
|
35
|
+
const mimeType = input.mimeType;
|
|
36
|
+
const requestedFileName = input.fileName;
|
|
37
|
+
if (
|
|
38
|
+
typeof mimeType !== "string" || !Object.prototype.hasOwnProperty.call(extensionByMimeType, mimeType) ||
|
|
39
|
+
typeof requestedFileName !== "string" || requestedFileName.length < 1 || requestedFileName.length > 120 ||
|
|
40
|
+
/[\\/\u0000-\u001f\u007f]/.test(requestedFileName)
|
|
41
|
+
) {
|
|
42
|
+
return response(badRequest, { error: "invalid_image" });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let member;
|
|
46
|
+
try {
|
|
47
|
+
// Resolve the session identity before elevating the upload mint. Never take
|
|
48
|
+
// a member, site, host, or folder identifier from the request body.
|
|
49
|
+
member = await currentMember.getMember({ fieldsets: ["FULL"] });
|
|
50
|
+
} catch {
|
|
51
|
+
return response(forbidden, { error: "member_required" });
|
|
52
|
+
}
|
|
53
|
+
const memberId = typeof member?._id === "string" ? member._id : "";
|
|
54
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(memberId)) {
|
|
55
|
+
return response(forbidden, { error: "member_required" });
|
|
56
|
+
}
|
|
57
|
+
if (String(member?.status ?? "").toUpperCase() === "BLOCKED") {
|
|
58
|
+
return response(forbidden, { error: "member_required" });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const identityPrefix = memberId.replace(/-/g, "").slice(0, 10).toLowerCase();
|
|
63
|
+
const uniqueSuffix = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
64
|
+
const fileName = `member-${identityPrefix}-profile-${uniqueSuffix}.${extensionByMimeType[mimeType]}`;
|
|
65
|
+
const generateUploadUrl = elevate(files.generateFileUploadUrl);
|
|
66
|
+
const result = await generateUploadUrl(mimeType, {
|
|
67
|
+
fileName,
|
|
68
|
+
private: false,
|
|
69
|
+
});
|
|
70
|
+
const uploadUrl = typeof result?.uploadUrl === "string" ? result.uploadUrl : "";
|
|
71
|
+
let parsedUrl;
|
|
72
|
+
try {
|
|
73
|
+
parsedUrl = new URL(uploadUrl);
|
|
74
|
+
} catch {
|
|
75
|
+
return response(serverError, { error: "upload_unavailable" });
|
|
76
|
+
}
|
|
77
|
+
const hostname = parsedUrl.hostname.toLowerCase();
|
|
78
|
+
if (
|
|
79
|
+
parsedUrl.protocol !== "https:" || parsedUrl.username || parsedUrl.password ||
|
|
80
|
+
(parsedUrl.port && parsedUrl.port !== "443") || parsedUrl.hash ||
|
|
81
|
+
!(hostname === "wixmp.com" || hostname.endsWith(".wixmp.com"))
|
|
82
|
+
) {
|
|
83
|
+
return response(serverError, { error: "upload_unavailable" });
|
|
84
|
+
}
|
|
85
|
+
return response(ok, { uploadUrl });
|
|
86
|
+
} catch {
|
|
87
|
+
// Do not return Wix errors or log the signed URL/token.
|
|
88
|
+
return response(serverError, { error: "upload_unavailable" });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -15,6 +15,8 @@ import { isHostedWorker } from "./hosted-checkout.js";
|
|
|
15
15
|
* time: the screens declare no `minBinaryVersion`, because a release takes its highest
|
|
16
16
|
* screen floor as its own and would then refuse every older binary. */
|
|
17
17
|
export const MEMBER_ACCOUNT_ROUTES_MIN_BINARY = "1.4.0";
|
|
18
|
+
export const MEMBER_MEDIA_FIELD_MIN_BINARY = "1.5.0";
|
|
19
|
+
export const MEMBER_PASSWORD_ACTIONS_MIN_BINARY = "1.5.0";
|
|
18
20
|
|
|
19
21
|
function atLeast(version: string, floor: string): boolean {
|
|
20
22
|
const parts = (v: string) => v.split("-")[0].split(".").map((p) => Number.parseInt(p, 10) || 0);
|
|
@@ -31,13 +33,19 @@ export interface MemberAccountRoutes {
|
|
|
31
33
|
addresses: boolean;
|
|
32
34
|
/** Saved cards on the member's own site (My Wallet). */
|
|
33
35
|
wallet: boolean;
|
|
36
|
+
/** Native MediaField image picking and presigned PUT support. */
|
|
37
|
+
mediaField: boolean;
|
|
38
|
+
/** Password credential actions introduced in HimalayaCore 1.5.0. */
|
|
39
|
+
passwordCredentials: boolean;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
export async function memberAccountRoutes(): Promise<MemberAccountRoutes> {
|
|
37
|
-
if (isHostedWorker()) return { addresses: false, wallet: false };
|
|
43
|
+
if (isHostedWorker()) return { addresses: false, wallet: false, mediaField: false, passwordCredentials: false };
|
|
38
44
|
const info = await getAppInfo().catch(() => undefined);
|
|
39
|
-
if (!info) return { addresses: false, wallet: false };
|
|
45
|
+
if (!info) return { addresses: false, wallet: false, mediaField: false, passwordCredentials: false };
|
|
40
46
|
const native = info.platform === "ios" || info.platform === "android" || info.platform === "macos";
|
|
41
47
|
const addresses = native && atLeast(info.binaryVersion ?? "", MEMBER_ACCOUNT_ROUTES_MIN_BINARY);
|
|
42
|
-
|
|
48
|
+
const mediaField = native && atLeast(info.binaryVersion ?? "", MEMBER_MEDIA_FIELD_MIN_BINARY);
|
|
49
|
+
const passwordCredentials = native && atLeast(info.binaryVersion ?? "", MEMBER_PASSWORD_ACTIONS_MIN_BINARY);
|
|
50
|
+
return { addresses, wallet: addresses && info.memberSiteBound === true, mediaField, passwordCredentials };
|
|
43
51
|
}
|
package/templates/nonprofit-cause/tier5-src/himi-branded-lite-account-settings/blocked-members.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { dispatch } from "@himalaya/dispatch";
|
|
2
|
+
import { defineService } from "@himalaya/state";
|
|
3
|
+
import { getMyMember, listCurrentMemberBlocking, unblockCurrentMember } from "@himalaya/wix-members";
|
|
4
|
+
import { getMyData } from "@himalaya/wix-users";
|
|
5
|
+
import { assertMemberAccess, memberAccess, type MemberAccess } from "../_shared/member-access.js";
|
|
6
|
+
import { copyParam, stashCopy } from "../_shared/himi-copy.js";
|
|
7
|
+
|
|
8
|
+
const PAGE_SIZE = 50;
|
|
9
|
+
const MAX_READBACK_PAGES = 20;
|
|
10
|
+
interface Row { memberId: string; label: string }
|
|
11
|
+
interface State {
|
|
12
|
+
status: "loading" | "loaded" | "error";
|
|
13
|
+
identityStatus: "unresolved" | "guest" | "error" | "member";
|
|
14
|
+
hydrated: boolean; notIdentityResolved: boolean;
|
|
15
|
+
guest: boolean; notGuest: boolean; identityError: boolean; notIdentityError: boolean;
|
|
16
|
+
notMember: boolean; isMember: boolean; notLoading: boolean; notErrored: boolean;
|
|
17
|
+
hasError: boolean; errorText: string;
|
|
18
|
+
blockedMembers: Row[]; hasMembers: boolean; noMembers: boolean; hideEmpty: boolean;
|
|
19
|
+
nextCursor: string; hasMore: boolean; noMore: boolean; loadingMore: boolean;
|
|
20
|
+
confirmOpen: boolean; noConfirm: boolean; confirmMemberId: string; message: string;
|
|
21
|
+
currentMemberEmail: string; currentMemberScope: string; currentMemberId: string;
|
|
22
|
+
}
|
|
23
|
+
interface Identity extends MemberAccess { memberId: string }
|
|
24
|
+
interface MyMemberResponse { member?: { id?: string } }
|
|
25
|
+
interface Page { blockedMemberIds?: string[]; pagingMetadata?: { cursors?: { next?: string } } }
|
|
26
|
+
|
|
27
|
+
const EMPTY: State = {
|
|
28
|
+
status: "loading", identityStatus: "unresolved", hydrated: false, notIdentityResolved: true,
|
|
29
|
+
guest: false, notGuest: true, identityError: false, notIdentityError: true,
|
|
30
|
+
notMember: true, isMember: false, notLoading: false, notErrored: true,
|
|
31
|
+
hasError: false, errorText: "", blockedMembers: [], hasMembers: false, noMembers: false, hideEmpty: true,
|
|
32
|
+
nextCursor: "", hasMore: false, noMore: true, loadingMore: false,
|
|
33
|
+
confirmOpen: false, noConfirm: true, confirmMemberId: "", message: "",
|
|
34
|
+
currentMemberEmail: "", currentMemberScope: "", currentMemberId: "",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function namedError(name: string, message: string): Error {
|
|
38
|
+
const error = new Error(message);
|
|
39
|
+
error.name = name;
|
|
40
|
+
return error;
|
|
41
|
+
}
|
|
42
|
+
function isNamed(error: unknown, name: string): boolean {
|
|
43
|
+
return (error as { name?: unknown } | null)?.name === name;
|
|
44
|
+
}
|
|
45
|
+
function clean(value: unknown): string { return typeof value === "string" ? value.trim() : ""; }
|
|
46
|
+
function sameIdentity(a: Identity, b: Identity): boolean {
|
|
47
|
+
return a.email === b.email && a.scope === b.scope && a.memberId === b.memberId;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function currentIdentity(): Promise<Identity | null> {
|
|
51
|
+
let raw: unknown;
|
|
52
|
+
let identityUnavailable = false;
|
|
53
|
+
try { raw = await dispatch("himi.wix.memberIdentity"); }
|
|
54
|
+
catch { identityUnavailable = true; }
|
|
55
|
+
const value = raw as { email?: unknown; payload?: { email?: unknown } } | null;
|
|
56
|
+
const emailValue = value?.email ?? value?.payload?.email;
|
|
57
|
+
let email = typeof emailValue === "string" ? emailValue.trim().toLowerCase() : "";
|
|
58
|
+
// Older native hosts predate the memberIdentity action but expose the profile through wix-users (the
|
|
59
|
+
// same rung memberAccess() uses). A host that fails BOTH ways is unavailable, never a guest.
|
|
60
|
+
if (!email && identityUnavailable) {
|
|
61
|
+
try {
|
|
62
|
+
const profile = await getMyData();
|
|
63
|
+
const legacy = profile.email ?? profile.contactEmail;
|
|
64
|
+
email = typeof legacy === "string" ? legacy.trim().toLowerCase() : "";
|
|
65
|
+
} catch { /* fall through to unavailable */ }
|
|
66
|
+
if (!email) throw namedError("IdentityUnavailable", "The current member identity could not be checked.");
|
|
67
|
+
}
|
|
68
|
+
if (!email) return null;
|
|
69
|
+
const access = await memberAccess();
|
|
70
|
+
if (!access.email || access.email !== email) throw namedError("IdentityUnavailable", "The current member identity could not be checked.");
|
|
71
|
+
const response = await getMyMember({ fieldsets: ["FULL"] }) as MyMemberResponse;
|
|
72
|
+
await assertMemberAccess(access);
|
|
73
|
+
const memberId = clean(response.member?.id);
|
|
74
|
+
if (!memberId) throw new Error("The signed-in member profile is unavailable.");
|
|
75
|
+
return { ...access, memberId };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function assertCurrentIdentity(expected: Identity): Promise<void> {
|
|
79
|
+
const current = await currentIdentity();
|
|
80
|
+
if (!current || !sameIdentity(current, expected)) throw namedError("MemberChanged", "The signed-in member changed. Reload this section.");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function rows(ids: string[]): Row[] {
|
|
84
|
+
const seen = new Set<string>();
|
|
85
|
+
const result: Row[] = [];
|
|
86
|
+
for (const raw of ids) {
|
|
87
|
+
const memberId = clean(raw);
|
|
88
|
+
if (!memberId || seen.has(memberId)) continue;
|
|
89
|
+
seen.add(memberId);
|
|
90
|
+
// The member-owned endpoint returns IDs only. getMember(id) is APP-authenticated;
|
|
91
|
+
// never call it with a member token or suggest that names were loaded.
|
|
92
|
+
result.push({ memberId, label: copyParam("accountSettings.blockedMemberLabel", "Member") + " · " + memberId });
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function withRows(state: State, blockedMembers: Row[], nextCursor: string): State {
|
|
98
|
+
return { ...state, blockedMembers, hasMembers: blockedMembers.length > 0,
|
|
99
|
+
noMembers: blockedMembers.length === 0,
|
|
100
|
+
hideEmpty: !(state.status === "loaded" && state.isMember && blockedMembers.length === 0),
|
|
101
|
+
nextCursor, hasMore: !!nextCursor, noMore: !nextCursor };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function memberState(identity: Identity): State {
|
|
105
|
+
return { ...EMPTY, status: "loaded", identityStatus: "member", hydrated: true, notIdentityResolved: false,
|
|
106
|
+
guest: false, notGuest: true, identityError: false, notIdentityError: true, notMember: false, isMember: true,
|
|
107
|
+
notLoading: true, notErrored: true, hasError: false, currentMemberEmail: identity.email,
|
|
108
|
+
currentMemberScope: identity.scope, currentMemberId: identity.memberId };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function failed(error: unknown): State {
|
|
112
|
+
const identityError = isNamed(error, "IdentityUnavailable") || isNamed(error, "MemberChanged");
|
|
113
|
+
const text = isNamed(error, "MemberChanged")
|
|
114
|
+
? copyParam("accountSettings.blockedMembersChanged", "Your account changed. Reload your blocked members.")
|
|
115
|
+
: identityError
|
|
116
|
+
? copyParam("accountSettings.blockedMembersIdentityError", "Your member identity could not be verified. Try again.")
|
|
117
|
+
: copyParam("accountSettings.blockedMembersError", "Blocked members could not be loaded. Try again.");
|
|
118
|
+
return { ...EMPTY, status: "error", identityStatus: identityError ? "error" : "member", hydrated: true,
|
|
119
|
+
notIdentityResolved: false, guest: false, notGuest: true, identityError, notIdentityError: !identityError,
|
|
120
|
+
notMember: false, isMember: true, notLoading: true, notErrored: false, hasError: true,
|
|
121
|
+
errorText: text, message: text };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function readAll(identity: Identity): Promise<string[]> {
|
|
125
|
+
const ids: string[] = [];
|
|
126
|
+
const seenCursors = new Set<string>();
|
|
127
|
+
let cursor = "";
|
|
128
|
+
for (let i = 0; i < MAX_READBACK_PAGES; i += 1) {
|
|
129
|
+
await assertCurrentIdentity(identity);
|
|
130
|
+
const page = await listCurrentMemberBlocking({ cursorPaging: { limit: PAGE_SIZE, ...(cursor ? { cursor } : {}) } }) as Page;
|
|
131
|
+
await assertCurrentIdentity(identity);
|
|
132
|
+
ids.push(...(page.blockedMemberIds ?? []));
|
|
133
|
+
const next = clean(page.pagingMetadata?.cursors?.next);
|
|
134
|
+
if (!next) return [...new Set(ids.map(clean).filter(Boolean))];
|
|
135
|
+
if (seenCursors.has(next)) throw new Error("Blocked member paging did not advance.");
|
|
136
|
+
seenCursors.add(next);
|
|
137
|
+
cursor = next;
|
|
138
|
+
}
|
|
139
|
+
throw new Error("The blocked member list is too large to verify safely.");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let loadVersion = 0;
|
|
143
|
+
let loadInFlight = false;
|
|
144
|
+
let pageInFlight = false;
|
|
145
|
+
let unblockInFlight = false;
|
|
146
|
+
type Actions = {
|
|
147
|
+
load: (state: State, arg?: unknown) => Promise<State>;
|
|
148
|
+
loadMore: (state: State) => Promise<State | undefined>;
|
|
149
|
+
requestUnblock: (state: State, arg?: unknown) => State;
|
|
150
|
+
cancelUnblock: (state: State) => State;
|
|
151
|
+
confirmUnblock: (state: State) => Promise<State | undefined>;
|
|
152
|
+
};
|
|
153
|
+
const svc: ReturnType<typeof defineService<State, Actions>> = defineService<State, Actions>("himiBrandedAccountBlockedMembers", {
|
|
154
|
+
state: { ...EMPTY },
|
|
155
|
+
notHydrated: ["status", "identityStatus", "hydrated", "notIdentityResolved", "guest", "notGuest", "identityError", "notIdentityError", "notMember", "isMember", "notLoading", "notErrored", "hasError", "errorText", "blockedMembers", "hasMembers", "noMembers", "hideEmpty", "nextCursor", "hasMore", "noMore", "loadingMore", "confirmOpen", "noConfirm", "confirmMemberId", "message", "currentMemberEmail", "currentMemberScope", "currentMemberId"],
|
|
156
|
+
actions: {
|
|
157
|
+
load: async (_state, arg): Promise<State> => {
|
|
158
|
+
if (loadInFlight) return svc.getState() as State;
|
|
159
|
+
loadInFlight = true;
|
|
160
|
+
stashCopy(arg);
|
|
161
|
+
const version = ++loadVersion;
|
|
162
|
+
try {
|
|
163
|
+
const identity = await currentIdentity();
|
|
164
|
+
if (version !== loadVersion) return svc.getState() as State;
|
|
165
|
+
if (!identity) return { ...EMPTY, status: "loaded", identityStatus: "guest", hydrated: true,
|
|
166
|
+
notIdentityResolved: false, guest: true, notGuest: false, notMember: true, notLoading: true, noMembers: false };
|
|
167
|
+
await assertCurrentIdentity(identity);
|
|
168
|
+
const page = await listCurrentMemberBlocking({ cursorPaging: { limit: PAGE_SIZE } }) as Page;
|
|
169
|
+
await assertCurrentIdentity(identity);
|
|
170
|
+
if (version !== loadVersion) return svc.getState() as State;
|
|
171
|
+
return withRows(memberState(identity), rows(page.blockedMemberIds ?? []), clean(page.pagingMetadata?.cursors?.next));
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (version !== loadVersion) return svc.getState() as State;
|
|
174
|
+
return failed(error);
|
|
175
|
+
} finally { loadInFlight = false; }
|
|
176
|
+
},
|
|
177
|
+
loadMore: async (state) => {
|
|
178
|
+
if (pageInFlight || state.status !== "loaded" || !state.isMember || !state.hasMore) return undefined;
|
|
179
|
+
pageInFlight = true;
|
|
180
|
+
// A newer load() supersedes this page: return undefined (commit nothing) instead of writing back
|
|
181
|
+
// the pre-await snapshot, which would replace the fresh list or its error state.
|
|
182
|
+
const version = loadVersion;
|
|
183
|
+
try {
|
|
184
|
+
const identity = await currentIdentity();
|
|
185
|
+
if (version !== loadVersion) return undefined;
|
|
186
|
+
const expected: Identity = { email: state.currentMemberEmail, scope: state.currentMemberScope, memberId: state.currentMemberId };
|
|
187
|
+
if (!identity || !sameIdentity(identity, expected)) return failed(namedError("MemberChanged", "The signed-in member changed. Reload this section."));
|
|
188
|
+
await assertCurrentIdentity(identity);
|
|
189
|
+
const page = await listCurrentMemberBlocking({ cursorPaging: { limit: PAGE_SIZE, cursor: state.nextCursor } }) as Page;
|
|
190
|
+
await assertCurrentIdentity(identity);
|
|
191
|
+
if (version !== loadVersion) return undefined;
|
|
192
|
+
const combined = rows([...state.blockedMembers.map((item) => item.memberId), ...(page.blockedMemberIds ?? [])]);
|
|
193
|
+
return { ...withRows(state, combined, clean(page.pagingMetadata?.cursors?.next)), loadingMore: false };
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (version !== loadVersion) return undefined;
|
|
196
|
+
if (isNamed(error, "MemberChanged") || isNamed(error, "IdentityUnavailable")) return failed(error);
|
|
197
|
+
return { ...state, status: "error", notLoading: true, notErrored: false, hasError: true,
|
|
198
|
+
errorText: copyParam("accountSettings.blockedMembersError", "Blocked members could not be loaded. Try again."),
|
|
199
|
+
blockedMembers: state.blockedMembers, hasMembers: state.blockedMembers.length > 0,
|
|
200
|
+
noMembers: false, hideEmpty: true, nextCursor: state.nextCursor, hasMore: true, noMore: false };
|
|
201
|
+
} finally { pageInFlight = false; }
|
|
202
|
+
},
|
|
203
|
+
requestUnblock: (state, arg) => {
|
|
204
|
+
const memberId = clean((arg as { memberId?: unknown } | undefined)?.memberId);
|
|
205
|
+
if (state.status !== "loaded" || !state.isMember || !memberId || !state.blockedMembers.some((item) => item.memberId === memberId)) return state;
|
|
206
|
+
return { ...state, confirmOpen: true, noConfirm: false, confirmMemberId: memberId,
|
|
207
|
+
message: "", errorText: "", hasError: false, notErrored: true };
|
|
208
|
+
},
|
|
209
|
+
cancelUnblock: (state) => ({ ...state, confirmOpen: false, noConfirm: true, confirmMemberId: "" }),
|
|
210
|
+
confirmUnblock: async (state) => {
|
|
211
|
+
if (unblockInFlight || !state.confirmOpen || state.status !== "loaded" || !state.isMember) return undefined;
|
|
212
|
+
const memberId = state.confirmMemberId;
|
|
213
|
+
if (!memberId || !state.blockedMembers.some((item) => item.memberId === memberId)) return undefined;
|
|
214
|
+
unblockInFlight = true;
|
|
215
|
+
let mutationAttempted = false;
|
|
216
|
+
try {
|
|
217
|
+
const identity = await currentIdentity();
|
|
218
|
+
const expected: Identity = { email: state.currentMemberEmail, scope: state.currentMemberScope, memberId: state.currentMemberId };
|
|
219
|
+
if (!identity || !sameIdentity(identity, expected)) return failed(namedError("MemberChanged", "The signed-in member changed. Reload this section."));
|
|
220
|
+
await assertCurrentIdentity(identity);
|
|
221
|
+
mutationAttempted = true;
|
|
222
|
+
loadVersion += 1; // a page requested before the unblock must not re-add the member...
|
|
223
|
+
await unblockCurrentMember(memberId);
|
|
224
|
+
await assertCurrentIdentity(identity);
|
|
225
|
+
const ids = await readAll(identity);
|
|
226
|
+
loadVersion += 1; // ...nor one requested while it was in flight: this authoritative list wins
|
|
227
|
+
if (ids.includes(memberId)) return { ...state, confirmOpen: true, noConfirm: false, hasError: true, notErrored: false,
|
|
228
|
+
errorText: copyParam("accountSettings.blockedUnblockStillListed", "This member still appears blocked. Reload the list before trying again.") };
|
|
229
|
+
const refreshed = rows(ids);
|
|
230
|
+
return { ...withRows({ ...state, confirmOpen: false, noConfirm: true, confirmMemberId: "", hasError: false,
|
|
231
|
+
notErrored: true, errorText: "", message: copyParam("accountSettings.blockedUnblocked", "Member unblocked.") }, refreshed, "") };
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (isNamed(error, "MemberChanged") || isNamed(error, "IdentityUnavailable")) return failed(error);
|
|
234
|
+
if (mutationAttempted) return { ...failed(error), errorText: copyParam("accountSettings.blockedUnblockUnconfirmed", "Could not confirm the updated blocked list. Reload and try again."), message: copyParam("accountSettings.blockedUnblockUnconfirmed", "Could not confirm the updated blocked list. Reload and try again."), confirmOpen: false, noConfirm: true };
|
|
235
|
+
return { ...state, confirmOpen: true, noConfirm: false, hasError: true, notErrored: false,
|
|
236
|
+
errorText: copyParam("accountSettings.blockedUnblockError", "This member could not be unblocked. Try again.") };
|
|
237
|
+
} finally { unblockInFlight = false; }
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
export { svc };
|