@wix/himalaya-cli 0.1024.0 → 0.1026.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/ts/src/schema.d.ts +12 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/core.mjs +6 -0
- package/dist/cli.mjs +29 -18
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/sdk-reference.json +1 -1
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +1031 -240
- package/templates/beauty-salon/tier5-src/_shared/detail-presence.ts +51 -0
- package/templates/beauty-salon/tier5-src/himi-branded-lite-orders/index.ts +4 -1
- package/templates/beauty-salon/tier5-src/wix-spaces-collection-detail/index.ts +9 -9
- package/templates/beauty-salon/tier5-src/wix-spaces-create-post/index.ts +14 -4
- package/templates/beauty-salon/tier5-src/wix-spaces-plan-detail/index.ts +7 -9
- package/templates/beauty-salon/tier5-src/wix-spaces-subscription-detail/index.ts +12 -11
- package/templates/beauty-salon/tier5-src/wix-spaces-view-profile/index.ts +22 -12
- package/templates/branded-lite/config.ts +3 -1
- package/templates/branded-lite/dev/index.ts +3455 -813
- package/templates/branded-lite/tier5-src/_shared/detail-presence.ts +51 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-orders/index.ts +4 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-portfolio-project/index.ts +7 -2
- package/templates/branded-lite/tier5-src/himi-branded-lite-simple-detail/index.ts +4 -3
- package/templates/branded-lite/tier5-src/wix-spaces-chat-thread/index.ts +17 -4
- package/templates/branded-lite/tier5-src/wix-spaces-collection-detail/index.ts +9 -9
- package/templates/branded-lite/tier5-src/wix-spaces-coupon-detail/index.ts +7 -9
- package/templates/branded-lite/tier5-src/wix-spaces-create-post/index.ts +14 -4
- package/templates/branded-lite/tier5-src/wix-spaces-food-order-detail/index.ts +6 -3
- package/templates/branded-lite/tier5-src/wix-spaces-form-detail/index.ts +7 -9
- package/templates/branded-lite/tier5-src/wix-spaces-forum-post/index.ts +7 -9
- package/templates/branded-lite/tier5-src/wix-spaces-invoice-detail/index.ts +7 -9
- package/templates/branded-lite/tier5-src/wix-spaces-member-detail/index.ts +8 -8
- package/templates/branded-lite/tier5-src/wix-spaces-photo-albums/index.ts +8 -2
- package/templates/branded-lite/tier5-src/wix-spaces-plan-detail/index.ts +7 -9
- package/templates/branded-lite/tier5-src/wix-spaces-subscription-detail/index.ts +12 -11
- package/templates/branded-lite/tier5-src/wix-spaces-view-profile/index.ts +22 -12
- package/templates/community-club/dev/index.ts +345 -97
- package/templates/community-club/tier5-src/wix-spaces-create-post/index.ts +14 -4
- package/templates/community-club/tier5-src/wix-spaces-view-profile/index.ts +22 -12
- package/templates/events-meetups/dev/index.ts +345 -97
- package/templates/events-meetups/tier5-src/wix-spaces-create-post/index.ts +14 -4
- package/templates/events-meetups/tier5-src/wix-spaces-view-profile/index.ts +22 -12
- package/templates/gym-fitness/dev/index.ts +1031 -240
- package/templates/gym-fitness/tier5-src/_shared/detail-presence.ts +51 -0
- package/templates/gym-fitness/tier5-src/himi-branded-lite-orders/index.ts +4 -1
- package/templates/gym-fitness/tier5-src/wix-spaces-collection-detail/index.ts +9 -9
- package/templates/gym-fitness/tier5-src/wix-spaces-create-post/index.ts +14 -4
- package/templates/gym-fitness/tier5-src/wix-spaces-plan-detail/index.ts +7 -9
- package/templates/gym-fitness/tier5-src/wix-spaces-subscription-detail/index.ts +12 -11
- package/templates/gym-fitness/tier5-src/wix-spaces-view-profile/index.ts +22 -12
- package/templates/nonprofit-cause/dev/index.ts +732 -188
- package/templates/nonprofit-cause/tier5-src/_shared/detail-presence.ts +51 -0
- package/templates/nonprofit-cause/tier5-src/wix-spaces-create-post/index.ts +14 -4
- package/templates/nonprofit-cause/tier5-src/wix-spaces-plan-detail/index.ts +7 -9
- package/templates/nonprofit-cause/tier5-src/wix-spaces-subscription-detail/index.ts +12 -11
- package/templates/nonprofit-cause/tier5-src/wix-spaces-view-profile/index.ts +22 -12
- package/templates/podcast-creator/dev/index.ts +942 -321
- package/templates/podcast-creator/tier5-src/_shared/detail-presence.ts +51 -0
- package/templates/podcast-creator/tier5-src/himi-branded-lite-simple-detail/index.ts +4 -3
- package/templates/podcast-creator/tier5-src/wix-spaces-create-post/index.ts +14 -4
- package/templates/podcast-creator/tier5-src/wix-spaces-plan-detail/index.ts +7 -9
- package/templates/podcast-creator/tier5-src/wix-spaces-subscription-detail/index.ts +12 -11
- package/templates/podcast-creator/tier5-src/wix-spaces-view-profile/index.ts +22 -12
- package/templates/portfolio-creative/dev/index.ts +676 -169
- package/templates/portfolio-creative/tier5-src/_shared/detail-presence.ts +51 -0
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-orders/index.ts +4 -1
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-portfolio-project/index.ts +7 -2
- package/templates/portfolio-creative/tier5-src/wix-spaces-collection-detail/index.ts +9 -9
- package/templates/portfolio-creative/tier5-src/wix-spaces-create-post/index.ts +14 -4
- package/templates/portfolio-creative/tier5-src/wix-spaces-view-profile/index.ts +22 -12
- package/templates/restaurant/dev/index.ts +529 -149
- package/templates/restaurant/tier5-src/_shared/detail-presence.ts +51 -0
- package/templates/restaurant/tier5-src/wix-spaces-create-post/index.ts +14 -4
- package/templates/restaurant/tier5-src/wix-spaces-food-order-detail/index.ts +6 -3
- package/templates/restaurant/tier5-src/wix-spaces-view-profile/index.ts +22 -12
- package/templates/retail-store/dev/index.ts +643 -148
- package/templates/retail-store/tier5-src/_shared/detail-presence.ts +51 -0
- package/templates/retail-store/tier5-src/himi-branded-lite-orders/index.ts +4 -1
- package/templates/retail-store/tier5-src/wix-spaces-collection-detail/index.ts +9 -9
- package/templates/retail-store/tier5-src/wix-spaces-create-post/index.ts +14 -4
- package/templates/retail-store/tier5-src/wix-spaces-view-profile/index.ts +22 -12
- package/templates/yoga-studio/dev/index.ts +711 -183
- package/templates/yoga-studio/tier5-src/_shared/detail-presence.ts +51 -0
- package/templates/yoga-studio/tier5-src/wix-spaces-create-post/index.ts +14 -4
- package/templates/yoga-studio/tier5-src/wix-spaces-plan-detail/index.ts +7 -9
- package/templates/yoga-studio/tier5-src/wix-spaces-subscription-detail/index.ts +12 -11
- package/templates/yoga-studio/tier5-src/wix-spaces-view-profile/index.ts +22 -12
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Whether a detail screen's entity is there — the worker half of the shared not-found
|
|
2
|
+
// state (the `detailNotFound` descriptor builder).
|
|
3
|
+
//
|
|
4
|
+
// A detail screen's descriptor asks two questions, both as `hiddenWhen` flags:
|
|
5
|
+
// • `noEntity` → hide the entity's content AND its actions (Like, Follow, Send…)
|
|
6
|
+
// • `hideNotFound` → hide the not-found state
|
|
7
|
+
// Four answers exist, and only one of them shows the not-found state:
|
|
8
|
+
// loading — a loading line (`hideLoading` false); no content, no actions, no verdict
|
|
9
|
+
// found — content + actions shown
|
|
10
|
+
// missing — not-found shown, content + actions hidden
|
|
11
|
+
// failed — a load-error line (`hideLoadError` false); no content, no actions
|
|
12
|
+
// A load that FAILS for any reason other than "not found" is `failed`, never `missing`:
|
|
13
|
+
// an offline device must not tell the user their invoice was deleted.
|
|
14
|
+
|
|
15
|
+
export type Presence = "loading" | "found" | "missing" | "failed";
|
|
16
|
+
|
|
17
|
+
export interface PresenceFlags {
|
|
18
|
+
found: boolean;
|
|
19
|
+
notFound: boolean;
|
|
20
|
+
noEntity: boolean;
|
|
21
|
+
hideNotFound: boolean;
|
|
22
|
+
hideLoading: boolean;
|
|
23
|
+
hideLoadError: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function presence(p: Presence): PresenceFlags {
|
|
27
|
+
return {
|
|
28
|
+
found: p === "found", notFound: p === "missing", noEntity: p !== "found", hideNotFound: p !== "missing",
|
|
29
|
+
hideLoading: p !== "loading", hideLoadError: p !== "failed",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The entity id the host baked into this detail screen (`himi.open` params). */
|
|
34
|
+
export function detailInputId(): string {
|
|
35
|
+
const v = (globalThis as { himalaya?: { input?: { id?: unknown } } }).himalaya?.input?.id;
|
|
36
|
+
return typeof v === "string" ? v.trim() : "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** True when a Wix read failed because the record does not exist: the SDK rejects
|
|
40
|
+
* with "HTTP <code>: <body>", so an HTTP 404, or Wix's `NOT_FOUND` error code. A bare
|
|
41
|
+
* "not found" in a message is NOT enough — "capability wix_api not found" or "Method
|
|
42
|
+
* not found" is a broken integration, and must not tell the user the record is gone. */
|
|
43
|
+
export function isNotFoundError(error: unknown): boolean {
|
|
44
|
+
const msg = String((error as { message?: unknown } | null)?.message ?? error ?? "");
|
|
45
|
+
return /\bHTTP\s*404\b/.test(msg) || /\bNOT_FOUND\b/.test(msg);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `missing` for a not-found failure, `failed` (claim nothing) otherwise. */
|
|
49
|
+
export function presenceAfterError(error: unknown): PresenceFlags {
|
|
50
|
+
return presence(isNotFoundError(error) ? "missing" : "failed");
|
|
51
|
+
}
|
|
@@ -54,6 +54,7 @@ interface State {
|
|
|
54
54
|
noEmpty: boolean;
|
|
55
55
|
hideList: boolean;
|
|
56
56
|
needsSignIn?: boolean;
|
|
57
|
+
noSignIn?: boolean;
|
|
57
58
|
nextCursor?: string;
|
|
58
59
|
complete?: boolean;
|
|
59
60
|
pageError?: boolean;
|
|
@@ -127,7 +128,9 @@ function derive(state: State): State {
|
|
|
127
128
|
notLoading: state.status !== "loading",
|
|
128
129
|
isErrored: state.status === "error",
|
|
129
130
|
notErrored: state.status !== "error",
|
|
130
|
-
|
|
131
|
+
// A guest gets the shared member gate, not the "Browse products" empty card.
|
|
132
|
+
noEmpty: state.status !== "ready" || hasRows || state.needsSignIn === true,
|
|
133
|
+
noSignIn: state.status !== "ready" || state.needsSignIn !== true,
|
|
131
134
|
hideList: state.status !== "ready" || state.needsSignIn === true,
|
|
132
135
|
};
|
|
133
136
|
}
|
|
@@ -5,6 +5,7 @@ import { defineService } from "@himalaya/state";
|
|
|
5
5
|
import { listProjectItems, listProjects, type WixPortfolioProjectItem } from "@himalaya/wix-portfolio";
|
|
6
6
|
import { wixMediaUrl, WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
|
|
7
7
|
import { copyParam, stashCopy } from "../_shared/himi-copy.js";
|
|
8
|
+
import { presence, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
8
9
|
|
|
9
10
|
interface MediaRow {
|
|
10
11
|
id: string;
|
|
@@ -20,7 +21,7 @@ interface MediaRow {
|
|
|
20
21
|
noMedia: boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
interface State {
|
|
24
|
+
interface State extends PresenceFlags {
|
|
24
25
|
status: "loading" | "ready" | "error";
|
|
25
26
|
id: string;
|
|
26
27
|
title: string;
|
|
@@ -99,6 +100,9 @@ function unavailableProject(state: State, status: "ready" | "error", id: string,
|
|
|
99
100
|
coverImage: "",
|
|
100
101
|
items: [],
|
|
101
102
|
emptyText,
|
|
103
|
+
// A project that is not there is `missing`; one that failed to load is not — the
|
|
104
|
+
// error card speaks, and nothing claims the project was removed.
|
|
105
|
+
...presence(status === "ready" ? "missing" : "failed"),
|
|
102
106
|
});
|
|
103
107
|
}
|
|
104
108
|
|
|
@@ -106,7 +110,7 @@ const svc = defineService<State, { load: (state: State, arg?: unknown) => Promis
|
|
|
106
110
|
state: derive({
|
|
107
111
|
status: "loading", id: "", title: "", initial: "", description: "", coverImage: "", items: [], emptyText: "",
|
|
108
112
|
isLoading: true, notLoading: false, isErrored: false, notErrored: true,
|
|
109
|
-
hasCover: false, noCover: true, hasItems: false, noItems: true, hideContent: true, noEmpty: true,
|
|
113
|
+
hasCover: false, noCover: true, hasItems: false, noItems: true, hideContent: true, noEmpty: true, ...presence("loading"),
|
|
110
114
|
}),
|
|
111
115
|
actions: {
|
|
112
116
|
load: async (state, arg) => {
|
|
@@ -132,6 +136,7 @@ const svc = defineService<State, { load: (state: State, arg?: unknown) => Promis
|
|
|
132
136
|
coverImage: wixMediaUrl(project.coverImage || "", WIX_DETAIL_IMAGE),
|
|
133
137
|
items,
|
|
134
138
|
emptyText: items.length ? "" : copyParam("portfolioProject.noMediaText", "This project has no published media yet."),
|
|
139
|
+
...presence("found"),
|
|
135
140
|
});
|
|
136
141
|
} catch {
|
|
137
142
|
return unavailableProject(state, "error", id, copyParam("portfolioProject.errorText", "This project could not be loaded. Try again."));
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
// a no-op action wired to CTAs so those buttons are truthful rather than
|
|
14
14
|
// silently absent capability).
|
|
15
15
|
import { defineService } from "@himalaya/state";
|
|
16
|
+
import { presence, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
16
17
|
|
|
17
|
-
interface S {
|
|
18
|
+
interface S extends PresenceFlags {
|
|
18
19
|
status: "ready";
|
|
19
20
|
id: string;
|
|
20
21
|
name: string;
|
|
@@ -39,11 +40,11 @@ const svc = defineService<S, {
|
|
|
39
40
|
load: (s: S) => S;
|
|
40
41
|
touch: (s: S, arg?: unknown) => S;
|
|
41
42
|
}>("himiBrandedSimpleDetail", {
|
|
42
|
-
state: { status: "ready", id: "", name: "", subtitle: "", hasId: false, noId: true, hint: "" },
|
|
43
|
+
state: { status: "ready", id: "", name: "", subtitle: "", hasId: false, noId: true, hint: "", ...presence("loading") },
|
|
43
44
|
actions: {
|
|
44
45
|
load: (s) => {
|
|
45
46
|
const id = inputStr("id");
|
|
46
|
-
return { ...s, id, name: inputStr("name"), subtitle: inputStr("subtitle"), hasId: !!id, noId: !id, hint: "" };
|
|
47
|
+
return { ...s, id, name: inputStr("name"), subtitle: inputStr("subtitle"), hasId: !!id, noId: !id, hint: "", ...presence(id ? "found" : "missing") };
|
|
47
48
|
},
|
|
48
49
|
touch: (s, arg) => {
|
|
49
50
|
const kind = (arg && typeof arg === "object" ? (arg as { kind?: string }).kind : typeof arg === "string" ? arg : undefined) ?? "";
|
|
@@ -7,12 +7,13 @@ import { defineService } from "@himalaya/state";
|
|
|
7
7
|
import { getMessages, sendChatMessage, getOrCreateDirectChatroom, type ChatMessage } from "@himalaya/wix-chat-web";
|
|
8
8
|
import { getMyData } from "@himalaya/wix-users";
|
|
9
9
|
import { formatMonthDayTime } from "../_shared/engine-intl.js";
|
|
10
|
+
import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
10
11
|
|
|
11
12
|
// leadFill gates a per-row right-align: " " for MY messages (a flexible gated
|
|
12
13
|
// spacer to the bubble's left → bubble hugs the right edge), "" for others
|
|
13
14
|
// (spacer collapses → bubble stays left). mine drives the bubble tint.
|
|
14
15
|
interface Row { id: string; text: string; when: string; leadFill: string; mine: string }
|
|
15
|
-
interface S { status: "loading" | "ready"; messages: Row[]; emptyText: string; draft: string; sendHint: string; resolvedChatroomId: string; myUserId: string }
|
|
16
|
+
interface S extends PresenceFlags { status: "loading" | "ready"; messages: Row[]; emptyText: string; draft: string; sendHint: string; resolvedChatroomId: string; myUserId: string }
|
|
16
17
|
|
|
17
18
|
function inputId(): string {
|
|
18
19
|
return (globalThis as { himalaya?: { input?: { id?: string } } }).himalaya?.input?.id ?? "";
|
|
@@ -63,16 +64,28 @@ const svc = defineService<S, {
|
|
|
63
64
|
setDraft: (s: S, a?: unknown) => S;
|
|
64
65
|
send: (s: S) => Promise<S>;
|
|
65
66
|
}>("spacesChatThread", {
|
|
66
|
-
state: { status: "loading", messages: [], emptyText: "", draft: "", sendHint: "", resolvedChatroomId: "", myUserId: "" },
|
|
67
|
+
state: { status: "loading", messages: [], emptyText: "", draft: "", sendHint: "", resolvedChatroomId: "", myUserId: "", ...presence("loading") },
|
|
67
68
|
actions: {
|
|
68
69
|
load: async (s) => {
|
|
69
70
|
const raw = inputId();
|
|
71
|
+
// No thread / recipient id (a stale link, a route opened directly) -> the shared
|
|
72
|
+
// not-found state, and no composer: there is nobody to send to.
|
|
73
|
+
if (!detailInputId()) return { ...s, status: "ready", messages: [], emptyText: "", ...presence("missing") };
|
|
70
74
|
// Resolve chatroom + my user id concurrently (myUserId tags own messages).
|
|
71
75
|
const [chatroomId, myUserId] = await Promise.all([resolveChatroomId(raw, inputKind()), resolveMyUserId()]);
|
|
76
|
+
// A recipient whose 1:1 chatroom could not be resolved is not a conversation yet:
|
|
77
|
+
// reload("") would return [] without throwing and publish a live, empty thread
|
|
78
|
+
// whose Send can only fail. Show the load-error line instead.
|
|
79
|
+
if (!chatroomId) return { ...s, status: "ready", resolvedChatroomId: "", myUserId, messages: [], emptyText: "", ...presence("failed") };
|
|
72
80
|
try {
|
|
73
81
|
const messages = await reload(chatroomId, myUserId);
|
|
74
|
-
return { ...s, status: "ready", resolvedChatroomId: chatroomId, myUserId, messages, emptyText: messages.length ? "" : "No messages yet. Say hello!" };
|
|
75
|
-
} catch
|
|
82
|
+
return { ...s, status: "ready", resolvedChatroomId: chatroomId, myUserId, messages, emptyText: messages.length ? "" : "No messages yet. Say hello!", ...presence("found") };
|
|
83
|
+
} catch (e) {
|
|
84
|
+
// A thread whose messages cannot be read is not a live, empty conversation: a 404
|
|
85
|
+
// is the shared not-found state, anything else the load-error line — never a
|
|
86
|
+
// "Say hello!" composer that can only fail.
|
|
87
|
+
return { ...s, status: "ready", resolvedChatroomId: chatroomId, myUserId, messages: [], emptyText: "", ...presenceAfterError(e) };
|
|
88
|
+
}
|
|
76
89
|
},
|
|
77
90
|
setDraft: (s, a) => ({ ...s, draft: val(a) }),
|
|
78
91
|
// REAL WRITE: send the typed message to the RESOLVED chatroom id, then re-read.
|
|
@@ -7,19 +7,17 @@ import {
|
|
|
7
7
|
getStoreCategory,
|
|
8
8
|
type WixCollection,
|
|
9
9
|
} from "@himalaya/wix-stores-collections";
|
|
10
|
+
import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
10
11
|
|
|
11
|
-
interface S { status: "loading" | "ready"; name: string; subtitle: string; description: string }
|
|
12
|
+
interface S extends PresenceFlags { status: "loading" | "ready"; name: string; subtitle: string; description: string }
|
|
12
13
|
|
|
13
|
-
function inputId(): string {
|
|
14
|
-
return (globalThis as { himalaya?: { input?: { id?: string } } }).himalaya?.input?.id ?? "";
|
|
15
|
-
}
|
|
16
14
|
|
|
17
15
|
const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesCollectionDetail", {
|
|
18
|
-
state: { status: "loading", name: "", subtitle: "", description: "" },
|
|
16
|
+
state: { status: "loading", name: "", subtitle: "", description: "", ...presence("loading") },
|
|
19
17
|
actions: {
|
|
20
18
|
load: async (s) => {
|
|
21
|
-
const id =
|
|
22
|
-
if (!id) return { ...s, status: "ready" };
|
|
19
|
+
const id = detailInputId();
|
|
20
|
+
if (!id) return { ...s, status: "ready", ...presence("missing") };
|
|
23
21
|
try {
|
|
24
22
|
const res = await getStoreCategory(id);
|
|
25
23
|
const c = res.category;
|
|
@@ -29,6 +27,7 @@ const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesCollectionDe
|
|
|
29
27
|
name: c.name || "Category",
|
|
30
28
|
subtitle: categoryCount(c),
|
|
31
29
|
description: c.description || "",
|
|
30
|
+
...presence("found"),
|
|
32
31
|
};
|
|
33
32
|
}
|
|
34
33
|
} catch { /* fall through to V1 */ }
|
|
@@ -43,9 +42,10 @@ const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesCollectionDe
|
|
|
43
42
|
name: c.name || "Collection",
|
|
44
43
|
subtitle: n != null ? `${n} ${n === 1 ? "product" : "products"}` : "",
|
|
45
44
|
description: c.description || "",
|
|
45
|
+
...presence("found"),
|
|
46
46
|
};
|
|
47
|
-
} catch {
|
|
48
|
-
return { ...s, status: "ready" };
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return { ...s, status: "ready", ...presenceAfterError(e) };
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
51
|
},
|
|
@@ -5,19 +5,17 @@
|
|
|
5
5
|
import { defineService } from "@himalaya/state";
|
|
6
6
|
import { getCoupon, type WixCoupon } from "@himalaya/wix-coupons";
|
|
7
7
|
import { writeText } from "@himalaya/clipboard";
|
|
8
|
+
import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
8
9
|
|
|
9
|
-
interface S { status: "loading" | "ready"; name: string; code: string; rawCode: string; discount: string; copyHint: string; hasCode: boolean; noCode: boolean }
|
|
10
|
+
interface S extends PresenceFlags { status: "loading" | "ready"; name: string; code: string; rawCode: string; discount: string; copyHint: string; hasCode: boolean; noCode: boolean }
|
|
10
11
|
|
|
11
|
-
function inputId(): string {
|
|
12
|
-
return (globalThis as { himalaya?: { input?: { id?: string } } }).himalaya?.input?.id ?? "";
|
|
13
|
-
}
|
|
14
12
|
|
|
15
13
|
const svc = defineService<S, { load: (s: S) => Promise<S>; copyCode: (s: S) => Promise<S> }>("spacesCouponDetail", {
|
|
16
|
-
state: { status: "loading", name: "", code: "", rawCode: "", discount: "", copyHint: "", hasCode: false, noCode: true },
|
|
14
|
+
state: { status: "loading", name: "", code: "", rawCode: "", discount: "", copyHint: "", hasCode: false, noCode: true, ...presence("loading") },
|
|
17
15
|
actions: {
|
|
18
16
|
load: async (s) => {
|
|
19
|
-
const id =
|
|
20
|
-
if (!id) return { ...s, status: "ready" };
|
|
17
|
+
const id = detailInputId();
|
|
18
|
+
if (!id) return { ...s, status: "ready", ...presence("missing") };
|
|
21
19
|
try {
|
|
22
20
|
const res = await getCoupon(id);
|
|
23
21
|
const c = (((res as unknown as { coupon?: WixCoupon }).coupon) ?? (res as unknown as WixCoupon)) as unknown as {
|
|
@@ -27,8 +25,8 @@ const svc = defineService<S, { load: (s: S) => Promise<S>; copyCode: (s: S) => P
|
|
|
27
25
|
const sp = c.specification;
|
|
28
26
|
const discount = sp?.percentOffRate ? `${sp.percentOffRate}% off` : sp?.moneyOffAmount ? `${sp.moneyOffAmount} off` : sp?.fixedPriceAmount ? `Fixed price ${sp.fixedPriceAmount}` : "";
|
|
29
27
|
const rawCode = c.code || "";
|
|
30
|
-
return { status: "ready", name: c.name || c.code || "Coupon", code: rawCode ? `Code: ${rawCode}` : "", rawCode, discount, copyHint: "", hasCode: !!rawCode, noCode: !rawCode };
|
|
31
|
-
} catch { return { ...s, status: "ready" }; }
|
|
28
|
+
return { status: "ready", name: c.name || c.code || "Coupon", code: rawCode ? `Code: ${rawCode}` : "", rawCode, discount, copyHint: "", hasCode: !!rawCode, noCode: !rawCode, ...presence("found") };
|
|
29
|
+
} catch (e) { return { ...s, status: "ready", ...presenceAfterError(e) }; }
|
|
32
30
|
},
|
|
33
31
|
copyCode: async (s) => {
|
|
34
32
|
if (!s.rawCode) return { ...s, copyHint: "No code to copy." };
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// create endpoint in the SDK; blog is the available authored-content write.)
|
|
5
5
|
import { defineService } from "@himalaya/state";
|
|
6
6
|
import { createDraftPost, publishDraftPost } from "@himalaya/wix-blog";
|
|
7
|
+
import { memberAccess } from "../_shared/member-access.js";
|
|
7
8
|
|
|
8
9
|
// `bodyFocus` is a focus TOKEN, not a flag: the body field focuses whenever this
|
|
9
10
|
// becomes a new non-empty value (`TextFieldProps.focus_path`). A monotonic counter
|
|
@@ -14,7 +15,11 @@ import { createDraftPost, publishDraftPost } from "@himalaya/wix-blog";
|
|
|
14
15
|
// the new state), so there is no way to observe an intermediate value while `submit`
|
|
15
16
|
// is awaiting the network. A type that promises a state nobody can ever see reads as
|
|
16
17
|
// an in-flight guard that exists — see `posting` below for the one that works.
|
|
17
|
-
|
|
18
|
+
// `needsSignIn` / `noSignIn` / `hideForm` drive the member gate: a guest sees the
|
|
19
|
+
// gate and NO form, a member sees the form, and until `load` has asked the
|
|
20
|
+
// host who is signed in, neither shows — so a guest can never start typing a post
|
|
21
|
+
// that would only fail on submit.
|
|
22
|
+
interface S { status: "ready"; title: string; body: string; hint: string; bodyFocus: string; needsSignIn: boolean; noSignIn: boolean; hideForm: boolean }
|
|
18
23
|
|
|
19
24
|
// The real double-press guard, module-local because it must live OUTSIDE published
|
|
20
25
|
// state for the reason above. One worker instance owns one composer, so a module flag
|
|
@@ -33,15 +38,20 @@ function val(arg?: unknown): string {
|
|
|
33
38
|
return typeof arg === "string" ? arg : ((arg as { value?: string; text?: string })?.value ?? (arg as { text?: string })?.text ?? "");
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
const svc = defineService<S, { setTitle: (s: S, a?: unknown) => S; setBody: (s: S, a?: unknown) => S; focusBody: (s: S) => S; submit: (s: S) => Promise<S> }>("spacesCreatePost", {
|
|
37
|
-
state: { status: "ready", title: "", body: "", hint: "Share something with the community.", bodyFocus: "" },
|
|
41
|
+
const svc = defineService<S, { load: (s: S) => Promise<S>; setTitle: (s: S, a?: unknown) => S; setBody: (s: S, a?: unknown) => S; focusBody: (s: S) => S; submit: (s: S) => Promise<S> }>("spacesCreatePost", {
|
|
42
|
+
state: { status: "ready", title: "", body: "", hint: "Share something with the community.", bodyFocus: "", needsSignIn: false, noSignIn: true, hideForm: true },
|
|
38
43
|
actions: {
|
|
44
|
+
load: async (s) => {
|
|
45
|
+
const { email } = await memberAccess();
|
|
46
|
+
return { ...s, needsSignIn: !email, noSignIn: !!email, hideForm: !email };
|
|
47
|
+
},
|
|
39
48
|
setTitle: (s, a) => ({ ...s, title: val(a) }),
|
|
40
49
|
setBody: (s, a) => ({ ...s, body: val(a) }),
|
|
41
50
|
// Title's Next key lands here. The keyboard stays up (that is the "next"
|
|
42
51
|
// contract) and the body field takes focus.
|
|
43
52
|
focusBody: (s) => ({ ...s, bodyFocus: bumpToken(s.bodyFocus) }),
|
|
44
53
|
submit: async (s) => {
|
|
54
|
+
if (s.needsSignIn) return { ...s, hint: "Sign in to post." };
|
|
45
55
|
const title = s.title.trim();
|
|
46
56
|
if (!title) return { ...s, hint: "Please enter a title." };
|
|
47
57
|
// A second press while the first is still in flight is a NO-OP, not a second
|
|
@@ -63,7 +73,7 @@ const svc = defineService<S, { setTitle: (s: S, a?: unknown) => S; setBody: (s:
|
|
|
63
73
|
// steals focus, so those transitions must clear it (see himi-store-account's
|
|
64
74
|
// NO_FOCUS on its mode switches). Same token, opposite fix; the deciding
|
|
65
75
|
// question is always whether the field stays mounted.
|
|
66
|
-
return { status: "ready", title: "", body: "", bodyFocus: s.bodyFocus, hint: `Posted “${title}” ✓ — it now appears in Blog.` };
|
|
76
|
+
return { ...s, status: "ready", title: "", body: "", bodyFocus: s.bodyFocus, hint: `Posted “${title}” ✓ — it now appears in Blog.` };
|
|
67
77
|
} catch {
|
|
68
78
|
return { ...s, status: "ready", hint: "Couldn't post — check your connection and try again." };
|
|
69
79
|
} finally {
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// Tier-5 bundle: Wix Spaces clone — explicit secure-site handoff.
|
|
2
2
|
// The owner-capable Restaurant Orders API must never be queried from a member device.
|
|
3
3
|
import { defineService } from "@himalaya/state";
|
|
4
|
+
import { presence, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
4
5
|
|
|
5
|
-
interface S { status: "loading" | "ready"; name: string; status_: string; total: string; items: string; message: string }
|
|
6
|
+
interface S extends PresenceFlags { status: "loading" | "ready"; name: string; status_: string; total: string; items: string; message: string }
|
|
6
7
|
|
|
7
8
|
const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesFoodOrderDetail", {
|
|
8
|
-
state: { status: "loading", name: "", status_: "", total: "", items: "", message: "" },
|
|
9
|
+
state: { status: "loading", name: "", status_: "", total: "", items: "", message: "", ...presence("loading") },
|
|
9
10
|
actions: {
|
|
10
|
-
|
|
11
|
+
// No order id → the shared not-found state; an id → the secure-site handoff.
|
|
12
|
+
load: async () => (!detailInputId() ? { status: "ready", name: "", status_: "", total: "", items: "", message: "", ...presence("missing") } : {
|
|
13
|
+
...presence("found"),
|
|
11
14
|
status: "ready",
|
|
12
15
|
name: "",
|
|
13
16
|
status_: "",
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
// Tier-5 bundle: Wix Spaces clone — Form detail (ON-DEVICE, site-scoped).
|
|
2
2
|
import { defineService } from "@himalaya/state";
|
|
3
3
|
import { getForm, type WixForm } from "@himalaya/wix-forms";
|
|
4
|
+
import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
4
5
|
|
|
5
|
-
interface S { status: "loading" | "ready"; name: string; subtitle: string; fields: string }
|
|
6
|
+
interface S extends PresenceFlags { status: "loading" | "ready"; name: string; subtitle: string; fields: string }
|
|
6
7
|
|
|
7
|
-
function inputId(): string {
|
|
8
|
-
return (globalThis as { himalaya?: { input?: { id?: string } } }).himalaya?.input?.id ?? "";
|
|
9
|
-
}
|
|
10
8
|
|
|
11
9
|
const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesFormDetail", {
|
|
12
|
-
state: { status: "loading", name: "", subtitle: "", fields: "" },
|
|
10
|
+
state: { status: "loading", name: "", subtitle: "", fields: "", ...presence("loading") },
|
|
13
11
|
actions: {
|
|
14
12
|
load: async (s) => {
|
|
15
|
-
const id =
|
|
16
|
-
if (!id) return { ...s, status: "ready" };
|
|
13
|
+
const id = detailInputId();
|
|
14
|
+
if (!id) return { ...s, status: "ready", ...presence("missing") };
|
|
17
15
|
try {
|
|
18
16
|
const res = await getForm(id);
|
|
19
17
|
const f = (((res as unknown as { form?: WixForm }).form) ?? (res as unknown as WixForm)) as unknown as {
|
|
@@ -23,8 +21,8 @@ const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesFormDetail",
|
|
|
23
21
|
};
|
|
24
22
|
const list = (f.fieldsV2 || f.fields || []) as Array<{ label?: string; inputOptions?: { label?: string } }>;
|
|
25
23
|
const fields = list.map((x) => `• ${x.label || x.inputOptions?.label || "Field"}`).join("\n");
|
|
26
|
-
return { status: "ready", name: f.name || f.title || "Form", subtitle: list.length ? `${list.length} fields` : "", fields };
|
|
27
|
-
} catch { return { ...s, status: "ready" }; }
|
|
24
|
+
return { status: "ready", name: f.name || f.title || "Form", subtitle: list.length ? `${list.length} fields` : "", fields, ...presence("found") };
|
|
25
|
+
} catch (e) { return { ...s, status: "ready", ...presenceAfterError(e) }; }
|
|
28
26
|
},
|
|
29
27
|
},
|
|
30
28
|
});
|
|
@@ -3,12 +3,10 @@
|
|
|
3
3
|
// post and "Post Comment" creates a real comment, via the Wix Forum + comments APIs.
|
|
4
4
|
import { defineService } from "@himalaya/state";
|
|
5
5
|
import { getPost, reactToPost, removePostReaction, createForumComment, type WixPost } from "@himalaya/wix-forum";
|
|
6
|
+
import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
6
7
|
|
|
7
|
-
interface S { status: "loading" | "ready"; id: string; title: string; author: string; subtitle: string; body: string; initial: string; likeHint: string; commentText: string; commentHint: string }
|
|
8
|
+
interface S extends PresenceFlags { status: "loading" | "ready"; id: string; title: string; author: string; subtitle: string; body: string; initial: string; likeHint: string; commentText: string; commentHint: string }
|
|
8
9
|
|
|
9
|
-
function inputId(): string {
|
|
10
|
-
return (globalThis as { himalaya?: { input?: { id?: string } } }).himalaya?.input?.id ?? "";
|
|
11
|
-
}
|
|
12
10
|
function plain(html?: string): string {
|
|
13
11
|
return (html || "").replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
14
12
|
}
|
|
@@ -23,11 +21,11 @@ const svc = defineService<S, {
|
|
|
23
21
|
setComment: (s: S, a?: unknown) => S;
|
|
24
22
|
comment: (s: S) => Promise<S>;
|
|
25
23
|
}>("spacesForumPost", {
|
|
26
|
-
state: { status: "loading", id: "", title: "", author: "", subtitle: "", body: "", initial: "", likeHint: "", commentText: "", commentHint: "" },
|
|
24
|
+
state: { status: "loading", id: "", title: "", author: "", subtitle: "", body: "", initial: "", likeHint: "", commentText: "", commentHint: "", ...presence("loading") },
|
|
27
25
|
actions: {
|
|
28
26
|
load: async (s) => {
|
|
29
|
-
const id =
|
|
30
|
-
if (!id) return { ...s, status: "ready" };
|
|
27
|
+
const id = detailInputId();
|
|
28
|
+
if (!id) return { ...s, status: "ready", ...presence("missing") };
|
|
31
29
|
try {
|
|
32
30
|
const res = await getPost(id);
|
|
33
31
|
const p = ((res as unknown as { post?: WixPost }).post) ?? (res as unknown as WixPost);
|
|
@@ -38,8 +36,8 @@ const svc = defineService<S, {
|
|
|
38
36
|
if (pp.viewCount != null) bits.push(`${pp.viewCount} views`);
|
|
39
37
|
return { status: "ready", id, title: pp.title || "Post", author, subtitle: bits.join(" · "),
|
|
40
38
|
body: plain(pp.contentText || pp.content) || "No content.", initial: (author[0] || "#").toUpperCase(),
|
|
41
|
-
likeHint: "", commentText: "", commentHint: "" };
|
|
42
|
-
} catch { return { ...s, status: "ready", id }; }
|
|
39
|
+
likeHint: "", commentText: "", commentHint: "", ...presence("found") };
|
|
40
|
+
} catch (e) { return { ...s, status: "ready", id, ...presenceAfterError(e) }; }
|
|
43
41
|
},
|
|
44
42
|
// REAL WRITE: react to / un-react to the post.
|
|
45
43
|
like: async (s) => {
|
|
@@ -2,19 +2,17 @@
|
|
|
2
2
|
import { defineService } from "@himalaya/state";
|
|
3
3
|
import { getInvoice, type WixInvoice } from "@himalaya/wix-invoices";
|
|
4
4
|
import { formatDateMedium } from "../_shared/engine-intl.js";
|
|
5
|
+
import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
5
6
|
|
|
6
|
-
interface S { status: "loading" | "ready"; name: string; status_: string; total: string; due: string }
|
|
7
|
+
interface S extends PresenceFlags { status: "loading" | "ready"; name: string; status_: string; total: string; due: string }
|
|
7
8
|
|
|
8
|
-
function inputId(): string {
|
|
9
|
-
return (globalThis as { himalaya?: { input?: { id?: string } } }).himalaya?.input?.id ?? "";
|
|
10
|
-
}
|
|
11
9
|
|
|
12
10
|
const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesInvoiceDetail", {
|
|
13
|
-
state: { status: "loading", name: "", status_: "", total: "", due: "" },
|
|
11
|
+
state: { status: "loading", name: "", status_: "", total: "", due: "", ...presence("loading") },
|
|
14
12
|
actions: {
|
|
15
13
|
load: async (s) => {
|
|
16
|
-
const id =
|
|
17
|
-
if (!id) return { ...s, status: "ready" };
|
|
14
|
+
const id = detailInputId();
|
|
15
|
+
if (!id) return { ...s, status: "ready", ...presence("missing") };
|
|
18
16
|
try {
|
|
19
17
|
const res = await getInvoice(id);
|
|
20
18
|
const v = (((res as unknown as { invoice?: WixInvoice }).invoice) ?? (res as unknown as WixInvoice)) as unknown as {
|
|
@@ -24,8 +22,8 @@ const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesInvoiceDetai
|
|
|
24
22
|
const due = v.dates?.dueDate ? `Due ${formatDateMedium(new Date(v.dates.dueDate))}` : "";
|
|
25
23
|
return { status: "ready", name: v.title || `Invoice ${v.number || ""}`.trim(),
|
|
26
24
|
status_: v.status ? v.status.charAt(0) + v.status.slice(1).toLowerCase() : "",
|
|
27
|
-
total: v.total?.formattedAmount || "", due };
|
|
28
|
-
} catch { return { ...s, status: "ready" }; }
|
|
25
|
+
total: v.total?.formattedAmount || "", due, ...presence("found") };
|
|
26
|
+
} catch (e) { return { ...s, status: "ready", ...presenceAfterError(e) }; }
|
|
29
27
|
},
|
|
30
28
|
},
|
|
31
29
|
});
|
|
@@ -12,8 +12,9 @@ import {
|
|
|
12
12
|
isFollowingMember,
|
|
13
13
|
type WixMember,
|
|
14
14
|
} from "@himalaya/wix-members";
|
|
15
|
+
import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
15
16
|
|
|
16
|
-
interface S {
|
|
17
|
+
interface S extends PresenceFlags {
|
|
17
18
|
status: "loading" | "ready";
|
|
18
19
|
id: string;
|
|
19
20
|
name: string;
|
|
@@ -29,9 +30,6 @@ interface S {
|
|
|
29
30
|
followCount: string;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
function inputId(): string {
|
|
33
|
-
return (globalThis as { himalaya?: { input?: { id?: string } } }).himalaya?.input?.id ?? "";
|
|
34
|
-
}
|
|
35
33
|
function name(m: WixMember): string {
|
|
36
34
|
const nick = m.profile?.nickname?.trim();
|
|
37
35
|
if (nick) return nick;
|
|
@@ -63,11 +61,12 @@ const svc = defineService<S, {
|
|
|
63
61
|
following: 0,
|
|
64
62
|
followLabel: "Follow",
|
|
65
63
|
followCount: "0 Followers · 0 Following",
|
|
64
|
+
...presence("loading"),
|
|
66
65
|
},
|
|
67
66
|
actions: {
|
|
68
67
|
load: async (s) => {
|
|
69
|
-
const id =
|
|
70
|
-
if (!id) return { ...s, status: "ready" };
|
|
68
|
+
const id = detailInputId();
|
|
69
|
+
if (!id) return { ...s, status: "ready", ...presence("missing") };
|
|
71
70
|
try {
|
|
72
71
|
const res = await getMember(id);
|
|
73
72
|
const m = ((res as unknown as { member?: WixMember }).member) ?? (res as unknown as WixMember);
|
|
@@ -102,9 +101,10 @@ const svc = defineService<S, {
|
|
|
102
101
|
following,
|
|
103
102
|
followLabel: followLabelFor(isFollowing),
|
|
104
103
|
followCount: followCountFor(followers, following),
|
|
104
|
+
...presence("found"),
|
|
105
105
|
};
|
|
106
|
-
} catch {
|
|
107
|
-
return { ...s, status: "ready", id };
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return { ...s, status: "ready", id, ...presenceAfterError(e) };
|
|
108
108
|
}
|
|
109
109
|
},
|
|
110
110
|
// REAL WRITE: two-state toggle. Flips the relationship and optimistically
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
// Tier-5 bundle: Wix Spaces clone — Photo Albums (ON-DEVICE, site-scoped).
|
|
2
2
|
// No albums API wired yet → renders the real empty state, never fabricated rows.
|
|
3
|
+
// The emptyTitle/emptyBody copy is the Spaces clone's LIST screen. An app that opens
|
|
4
|
+
// this for ONE album (branded-lite's gallery "Open album", which passes the album id)
|
|
5
|
+
// renders its own single-album copy and gates it on `noEntity`; opened with no id at
|
|
6
|
+
// all it shows the shared not-found state.
|
|
3
7
|
import { defineService } from "@himalaya/state";
|
|
8
|
+
import { presence, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
4
9
|
|
|
5
|
-
interface S { status: "ready"; emptyTitle: string; emptyBody: string }
|
|
10
|
+
interface S extends PresenceFlags { status: "ready"; emptyTitle: string; emptyBody: string }
|
|
6
11
|
|
|
7
12
|
const svc = defineService<S, { load: (s: S) => S }>("spacesPhotoAlbums", {
|
|
8
13
|
state: {
|
|
9
14
|
status: "ready",
|
|
10
15
|
emptyTitle: "No albums yet",
|
|
11
16
|
emptyBody: "Photo albums shared on this site will appear here.",
|
|
17
|
+
...presence("loading"),
|
|
12
18
|
},
|
|
13
|
-
actions: { load: (s) => s },
|
|
19
|
+
actions: { load: (s) => ({ ...s, ...presence(detailInputId() ? "found" : "missing") }) },
|
|
14
20
|
});
|
|
15
21
|
export { svc };
|
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
// himalaya.input.id carries the plan id; fetched via getPlan.
|
|
3
3
|
import { defineService } from "@himalaya/state";
|
|
4
4
|
import { getPlan, type WixPlan } from "@himalaya/wix-pricing-plans";
|
|
5
|
+
import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
5
6
|
|
|
6
|
-
interface S { status: "loading" | "ready"; name: string; price: string; description: string; perks: string }
|
|
7
|
+
interface S extends PresenceFlags { status: "loading" | "ready"; name: string; price: string; description: string; perks: string }
|
|
7
8
|
|
|
8
|
-
function inputId(): string {
|
|
9
|
-
return (globalThis as { himalaya?: { input?: { id?: string } } }).himalaya?.input?.id ?? "";
|
|
10
|
-
}
|
|
11
9
|
function priceOf(p: WixPlan): string {
|
|
12
10
|
const m = p.pricing?.price as unknown as { formattedAmount?: string; amount?: string; currency?: string } | undefined;
|
|
13
11
|
if (!m) return p.pricing?.freeTrialDays ? "Free trial" : "";
|
|
@@ -15,17 +13,17 @@ function priceOf(p: WixPlan): string {
|
|
|
15
13
|
}
|
|
16
14
|
|
|
17
15
|
const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesPlanDetail", {
|
|
18
|
-
state: { status: "loading", name: "", price: "", description: "", perks: "" },
|
|
16
|
+
state: { status: "loading", name: "", price: "", description: "", perks: "", ...presence("loading") },
|
|
19
17
|
actions: {
|
|
20
18
|
load: async (s) => {
|
|
21
|
-
const id =
|
|
22
|
-
if (!id) return { ...s, status: "ready" };
|
|
19
|
+
const id = detailInputId();
|
|
20
|
+
if (!id) return { ...s, status: "ready", ...presence("missing") };
|
|
23
21
|
try {
|
|
24
22
|
const res = await getPlan(id);
|
|
25
23
|
const p = ((res as unknown as { plan?: WixPlan }).plan) ?? (res as unknown as WixPlan);
|
|
26
24
|
const perks = (p.perks?.values ?? []).map((v) => `• ${v}`).join("\n");
|
|
27
|
-
return { status: "ready", name: p.name || "Plan", price: priceOf(p), description: p.description || "", perks };
|
|
28
|
-
} catch { return { ...s, status: "ready" }; }
|
|
25
|
+
return { status: "ready", name: p.name || "Plan", price: priceOf(p), description: p.description || "", perks, ...presence("found") };
|
|
26
|
+
} catch (e) { return { ...s, status: "ready", ...presenceAfterError(e) }; }
|
|
29
27
|
},
|
|
30
28
|
},
|
|
31
29
|
});
|
|
@@ -2,23 +2,23 @@
|
|
|
2
2
|
import { defineService } from "@himalaya/state";
|
|
3
3
|
import { getPlanOrder, type WixPlanOrder } from "@himalaya/wix-pricing-plans";
|
|
4
4
|
import { formatDateMedium } from "../_shared/engine-intl.js";
|
|
5
|
+
import { presence, presenceAfterError, detailInputId, type PresenceFlags } from "../_shared/detail-presence.js";
|
|
5
6
|
|
|
6
|
-
interface S { status: "loading" | "ready"; name: string; subtitle: string; price: string; validity: string }
|
|
7
|
+
interface S extends PresenceFlags { status: "loading" | "ready"; name: string; subtitle: string; price: string; validity: string }
|
|
7
8
|
|
|
8
|
-
function inputId(): string {
|
|
9
|
-
return (globalThis as { himalaya?: { input?: { id?: string } } }).himalaya?.input?.id ?? "";
|
|
10
|
-
}
|
|
11
9
|
|
|
10
|
+
// Shown by hosts without the shared not-found state (the Spaces clone renders `subtitle`
|
|
11
|
+
// directly); branded-lite hides the whole content block behind `notFound`.
|
|
12
12
|
const EMPTY_DETAIL = "Open a subscription from My Subscriptions — detail needs a plan-order id.";
|
|
13
13
|
const LOAD_FAILED = "This subscription could not be loaded. Try again from My Subscriptions.";
|
|
14
14
|
|
|
15
15
|
const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesSubscriptionDetail", {
|
|
16
|
-
state: { status: "loading", name: "", subtitle: "", price: "", validity: "" },
|
|
16
|
+
state: { status: "loading", name: "", subtitle: "", price: "", validity: "", ...presence("loading") },
|
|
17
17
|
actions: {
|
|
18
18
|
load: async (s) => {
|
|
19
|
-
const id =
|
|
20
|
-
//
|
|
21
|
-
if (!id) return { status: "ready", name: "", subtitle: EMPTY_DETAIL, price: "", validity: "" };
|
|
19
|
+
const id = detailInputId();
|
|
20
|
+
// No order id (a stale link, a route opened directly) → the shared not-found state.
|
|
21
|
+
if (!id) return { status: "ready", name: "", subtitle: EMPTY_DETAIL, price: "", validity: "", ...presence("missing") };
|
|
22
22
|
try {
|
|
23
23
|
const res = await getPlanOrder(id);
|
|
24
24
|
const o = (((res as unknown as { order?: WixPlanOrder }).order) ?? (res as unknown as WixPlanOrder)) as unknown as {
|
|
@@ -29,9 +29,10 @@ const svc = defineService<S, { load: (s: S) => Promise<S> }>("spacesSubscription
|
|
|
29
29
|
const price = o.pricing?.prices?.[0]?.price?.formattedTotal || "";
|
|
30
30
|
const end = o.currentCycle?.endedDate ? `Renews ${formatDateMedium(new Date(o.currentCycle.endedDate))}` : "";
|
|
31
31
|
return { status: "ready", name: o.planName || "Subscription",
|
|
32
|
-
subtitle: o.status ? o.status.charAt(0) + o.status.slice(1).toLowerCase() : "", price, validity: end };
|
|
33
|
-
} catch {
|
|
34
|
-
|
|
32
|
+
subtitle: o.status ? o.status.charAt(0) + o.status.slice(1).toLowerCase() : "", price, validity: end, ...presence("found") };
|
|
33
|
+
} catch (e) {
|
|
34
|
+
const flags = presenceAfterError(e);
|
|
35
|
+
return { status: "ready", name: "", subtitle: flags.notFound ? "" : LOAD_FAILED, price: "", validity: "", ...flags };
|
|
35
36
|
}
|
|
36
37
|
},
|
|
37
38
|
},
|