@wix/himalaya-cli 0.1387.0 → 0.1389.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/stdlib/flows/iap-checkout/src/commerce-contract.d.ts +24 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/commerce-core.d.ts +3 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/index.d.ts +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/video/src/display.d.ts +66 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/video/src/index.d.ts +185 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +57 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +57 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-video.mjs +324 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/package.json +4 -0
- package/dist/cli.mjs +10 -5
- package/guides/iap-screen-adoption.md +53 -1
- package/guides/importable-sdks.md +1 -0
- package/guides/sdk-reference.json +261 -2
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +193 -1
- package/templates/beauty-salon/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/beauty-salon/tier5-src/_shared/iap-worker.ts +84 -3
- package/templates/beauty-salon/tier5-src/_shared/video-caption.ts +21 -0
- package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +31 -1
- package/templates/beauty-salon/tier5-src/himi-store-account/index.ts +36 -1
- package/templates/beauty-salon-owner/config.ts +11 -0
- package/templates/beauty-salon-owner/dev/index.ts +540 -0
- package/templates/beauty-salon-owner/himalaya.content.json +6 -0
- package/templates/beauty-salon-owner/platforms.json +2 -1
- package/templates/beauty-salon-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
- package/templates/branded-lite/dev/index.ts +1212 -54
- package/templates/branded-lite/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/branded-lite/tier5-src/_shared/iap-worker.ts +84 -3
- package/templates/branded-lite/tier5-src/_shared/video-caption.ts +21 -0
- package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +31 -1
- package/templates/branded-lite/tier5-src/himi-branded-lite-video/index.ts +23 -32
- package/templates/branded-lite/tier5-src/himi-branded-lite-videos/index.ts +44 -38
- package/templates/branded-lite/tier5-src/himi-store-account/index.ts +36 -1
- package/templates/community-club/dev/index.ts +193 -1
- package/templates/community-club/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/community-club/tier5-src/_shared/iap-worker.ts +84 -3
- package/templates/community-club/tier5-src/_shared/video-caption.ts +21 -0
- package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +31 -1
- package/templates/community-club/tier5-src/himi-store-account/index.ts +36 -1
- package/templates/community-club-owner/config.ts +11 -0
- package/templates/community-club-owner/dev/index.ts +540 -0
- package/templates/community-club-owner/himalaya.content.json +6 -0
- package/templates/community-club-owner/platforms.json +2 -1
- package/templates/community-club-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/community-club-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
- package/templates/events-meetups/dev/index.ts +193 -1
- package/templates/events-meetups/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/events-meetups/tier5-src/_shared/iap-worker.ts +84 -3
- package/templates/events-meetups/tier5-src/_shared/video-caption.ts +21 -0
- package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +31 -1
- package/templates/events-meetups/tier5-src/himi-store-account/index.ts +36 -1
- package/templates/events-meetups-owner/config.ts +11 -0
- package/templates/events-meetups-owner/dev/index.ts +540 -0
- package/templates/events-meetups-owner/himalaya.content.json +6 -0
- package/templates/events-meetups-owner/platforms.json +2 -1
- package/templates/events-meetups-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/events-meetups-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
- package/templates/gym-fitness/dev/index.ts +193 -1
- package/templates/gym-fitness/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/gym-fitness/tier5-src/_shared/iap-worker.ts +84 -3
- package/templates/gym-fitness/tier5-src/_shared/video-caption.ts +21 -0
- package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +31 -1
- package/templates/gym-fitness/tier5-src/himi-store-account/index.ts +36 -1
- package/templates/gym-fitness-owner/config.ts +11 -0
- package/templates/gym-fitness-owner/dev/index.ts +540 -0
- package/templates/gym-fitness-owner/himalaya.content.json +6 -0
- package/templates/gym-fitness-owner/platforms.json +2 -1
- package/templates/gym-fitness-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
- package/templates/nonprofit-cause/dev/index.ts +193 -1
- package/templates/nonprofit-cause/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/nonprofit-cause/tier5-src/_shared/iap-worker.ts +84 -3
- package/templates/nonprofit-cause/tier5-src/_shared/video-caption.ts +21 -0
- package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +31 -1
- package/templates/nonprofit-cause/tier5-src/himi-store-account/index.ts +36 -1
- package/templates/nonprofit-cause-owner/config.ts +11 -0
- package/templates/nonprofit-cause-owner/dev/index.ts +540 -0
- package/templates/nonprofit-cause-owner/himalaya.content.json +6 -0
- package/templates/nonprofit-cause-owner/platforms.json +2 -1
- package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
- package/templates/owner-lite/config.ts +11 -0
- package/templates/owner-lite/dev/index.ts +540 -0
- package/templates/owner-lite/himalaya.content.json +6 -0
- package/templates/owner-lite/platforms.json +2 -1
- package/templates/owner-lite/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/owner-lite/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
- package/templates/podcast-creator/dev/index.ts +193 -1
- package/templates/podcast-creator/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/podcast-creator/tier5-src/_shared/iap-worker.ts +84 -3
- package/templates/podcast-creator/tier5-src/_shared/video-caption.ts +21 -0
- package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +31 -1
- package/templates/podcast-creator/tier5-src/himi-store-account/index.ts +36 -1
- package/templates/podcast-creator-owner/config.ts +11 -0
- package/templates/podcast-creator-owner/dev/index.ts +540 -0
- package/templates/podcast-creator-owner/himalaya.content.json +6 -0
- package/templates/podcast-creator-owner/platforms.json +2 -1
- package/templates/podcast-creator-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
- package/templates/portfolio-creative/dev/index.ts +193 -1
- package/templates/portfolio-creative/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/portfolio-creative/tier5-src/_shared/iap-worker.ts +84 -3
- package/templates/portfolio-creative/tier5-src/_shared/video-caption.ts +21 -0
- package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +31 -1
- package/templates/portfolio-creative/tier5-src/himi-store-account/index.ts +36 -1
- package/templates/portfolio-creative-owner/config.ts +11 -0
- package/templates/portfolio-creative-owner/dev/index.ts +540 -0
- package/templates/portfolio-creative-owner/himalaya.content.json +6 -0
- package/templates/portfolio-creative-owner/platforms.json +2 -1
- package/templates/portfolio-creative-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
- package/templates/restaurant/dev/index.ts +193 -1
- package/templates/restaurant/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/restaurant/tier5-src/_shared/iap-worker.ts +84 -3
- package/templates/restaurant/tier5-src/_shared/video-caption.ts +21 -0
- package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +31 -1
- package/templates/restaurant/tier5-src/himi-store-account/index.ts +36 -1
- package/templates/restaurant-owner/config.ts +11 -0
- package/templates/restaurant-owner/dev/index.ts +540 -0
- package/templates/restaurant-owner/himalaya.content.json +6 -0
- package/templates/restaurant-owner/platforms.json +2 -1
- package/templates/restaurant-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/restaurant-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
- package/templates/retail-store/dev/index.ts +193 -1
- package/templates/retail-store/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/retail-store/tier5-src/_shared/iap-worker.ts +84 -3
- package/templates/retail-store/tier5-src/_shared/video-caption.ts +21 -0
- package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +31 -1
- package/templates/retail-store/tier5-src/himi-store-account/index.ts +36 -1
- package/templates/retail-store-owner/config.ts +11 -0
- package/templates/retail-store-owner/dev/index.ts +540 -0
- package/templates/retail-store-owner/himalaya.content.json +6 -0
- package/templates/retail-store-owner/platforms.json +2 -1
- package/templates/retail-store-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/retail-store-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
- package/templates/yoga-studio/dev/index.ts +193 -1
- package/templates/yoga-studio/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/yoga-studio/tier5-src/_shared/iap-worker.ts +84 -3
- package/templates/yoga-studio/tier5-src/_shared/video-caption.ts +21 -0
- package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +31 -1
- package/templates/yoga-studio/tier5-src/himi-store-account/index.ts +36 -1
- package/templates/yoga-studio-owner/config.ts +11 -0
- package/templates/yoga-studio-owner/dev/index.ts +540 -0
- package/templates/yoga-studio-owner/himalaya.content.json +6 -0
- package/templates/yoga-studio-owner/platforms.json +2 -1
- package/templates/yoga-studio-owner/tier5-src/_shared/collection-contract.ts +1 -1
- package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
- package/templates/branded-lite/tier5-src/_shared/vod.ts +0 -71
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// Tier-5 bundle: __HIMI_APP_NAME__ — WRIST. The worker behind the paired Apple Watch's two
|
|
2
|
+
// screens, run headlessly ON THE PHONE by the app's watch brain (companion mode): the watch
|
|
3
|
+
// never holds a Wix session and never runs JS, it renders the literal descriptor the phone
|
|
4
|
+
// resolves from this state.
|
|
5
|
+
//
|
|
6
|
+
// Two owner moments, nothing else: "who's next and when" (nextUp + the rest of today) and
|
|
7
|
+
// "accept or decline a booking request" (requests, one at a time). Confirm / decline call
|
|
8
|
+
// the same @himalaya/wix-bookings writes the Wix Owner booking screen uses, but per request:
|
|
9
|
+
// that worker's actions read a screen-param id and a single screen revision, and the wrist
|
|
10
|
+
// holds a QUEUE of requests, each with its own revision.
|
|
11
|
+
//
|
|
12
|
+
// Every write carries the siteId the watch rendered: a site switched on the phone between
|
|
13
|
+
// render and tap must never confirm another business's booking.
|
|
14
|
+
|
|
15
|
+
import { defineService } from "@himalaya/state";
|
|
16
|
+
import { queryBookings, confirmBooking, declineBooking } from "@himalaya/wix-bookings";
|
|
17
|
+
import { isBridgeUnavailable } from "../_shared/owner-load-error.js";
|
|
18
|
+
import { readSelectedSite, selectedSiteName } from "../_shared/owner-selected-site.js";
|
|
19
|
+
|
|
20
|
+
type Rec = Record<string, unknown>;
|
|
21
|
+
|
|
22
|
+
export interface WristRow {
|
|
23
|
+
bookingId: string;
|
|
24
|
+
name: string; // client first name (or "Client")
|
|
25
|
+
service: string; // booked service title
|
|
26
|
+
startIso: string;
|
|
27
|
+
endIso: string;
|
|
28
|
+
timeLabel: string; // "10:30"
|
|
29
|
+
rangeLabel: string; // "10:30 – 11:30"
|
|
30
|
+
countdown: string; // "in 21 min" / "in 1 h 5 min" / "now" — relative to the last load
|
|
31
|
+
whenLabel: string; // "Today" / "Tomorrow" / "Thu 16 Oct" — the day it falls on
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface WristRequest extends WristRow {
|
|
35
|
+
revision: string;
|
|
36
|
+
durationMin: number;
|
|
37
|
+
dayLabel: string; // "Thu 14:00"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface WristState {
|
|
41
|
+
status: "loading" | "ready" | "error" | "signedOut" | "noBookings";
|
|
42
|
+
siteId: string;
|
|
43
|
+
siteName: string;
|
|
44
|
+
/** Always an object — an absent one is EMPTY_ROW behind `hasNextUp` — so every binding
|
|
45
|
+
* path resolves on every state (a null parent left them unresolved). */
|
|
46
|
+
nextUp: WristRow;
|
|
47
|
+
hasNextUp: boolean;
|
|
48
|
+
later: WristRow[];
|
|
49
|
+
requests: WristRequest[];
|
|
50
|
+
requestCount: number;
|
|
51
|
+
requestBadge: string; // "2 requests" / "1 request" / ""
|
|
52
|
+
hasRequests: boolean;
|
|
53
|
+
noRequests: boolean;
|
|
54
|
+
/** Hide "No requests waiting": there are requests, or the screen is a degrade state. */
|
|
55
|
+
hideNoRequests: boolean;
|
|
56
|
+
requestIndex: number;
|
|
57
|
+
requestPosition: string; // "Request 1 of 2"
|
|
58
|
+
current: WristRequest;
|
|
59
|
+
doneToday: boolean;
|
|
60
|
+
tomorrowFirst: WristRow;
|
|
61
|
+
hasTomorrowFirst: boolean;
|
|
62
|
+
/** Hide the "next booking" block: there is a next-up today, or nothing upcoming at all. */
|
|
63
|
+
hideTomorrow: boolean;
|
|
64
|
+
inFlight: boolean;
|
|
65
|
+
/** Decline is two taps: `askDecline` reveals the confirm row, `decline` writes. */
|
|
66
|
+
confirmingDecline: boolean;
|
|
67
|
+
notConfirmingDecline: boolean;
|
|
68
|
+
actionError: string;
|
|
69
|
+
message: string; // Home's one-line state copy (incl. "You're done for today")
|
|
70
|
+
/** Only the copy for a state the owner must act on (signed out, no Bookings, error) — the
|
|
71
|
+
* request screen's line, where "done for today" would be wrong. */
|
|
72
|
+
statusMessage: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const COPY = {
|
|
76
|
+
signedOut: "Sign in on your iPhone",
|
|
77
|
+
noBookings: "Bookings isn't set up on this site",
|
|
78
|
+
doneToday: "You're done for today",
|
|
79
|
+
noRequests: "No requests waiting",
|
|
80
|
+
changed: "This request changed — check it on your iPhone",
|
|
81
|
+
error: "Open Owner on your iPhone",
|
|
82
|
+
writeFailed: "Couldn't update it — check it on your iPhone",
|
|
83
|
+
} as const;
|
|
84
|
+
|
|
85
|
+
const PENDING = new Set(["PENDING", "PENDING_APPROVAL", "WAITING_LIST"]);
|
|
86
|
+
const LIMIT = 50;
|
|
87
|
+
|
|
88
|
+
function str(r: Rec | undefined, ...paths: string[]): string {
|
|
89
|
+
for (const path of paths) {
|
|
90
|
+
let v: unknown = r;
|
|
91
|
+
for (const k of path.split(".")) v = v && typeof v === "object" ? (v as Rec)[k] : undefined;
|
|
92
|
+
if (typeof v === "string" && v) return v;
|
|
93
|
+
if (typeof v === "number") return String(v);
|
|
94
|
+
}
|
|
95
|
+
return "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function hhmm(d: Date): string {
|
|
99
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
103
|
+
|
|
104
|
+
function sameLocalDay(a: Date, b: Date): boolean {
|
|
105
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** "in 21 min" / "in 1 h 5 min" / "now". The phone reloads while the watch is visible, so a
|
|
109
|
+
* minute-granularity label stays honest without an OS-ticked timer on the wrist. */
|
|
110
|
+
export function countdown(startMs: number, nowMs: number): string {
|
|
111
|
+
const minutes = Math.round((startMs - nowMs) / 60000);
|
|
112
|
+
if (minutes <= 0) return "now";
|
|
113
|
+
if (minutes < 60) return `in ${minutes} min`;
|
|
114
|
+
const h = Math.floor(minutes / 60), m = minutes % 60;
|
|
115
|
+
return m ? `in ${h} h ${m} min` : `in ${h} h`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function row(b: Rec, nowMs = Date.now()): WristRow {
|
|
119
|
+
const startIso = str(b, "startDate", "bookedEntity.slot.startDate");
|
|
120
|
+
const endIso = str(b, "endDate", "bookedEntity.slot.endDate");
|
|
121
|
+
const start = new Date(startIso);
|
|
122
|
+
const end = endIso ? new Date(endIso) : null;
|
|
123
|
+
return {
|
|
124
|
+
bookingId: str(b, "id"),
|
|
125
|
+
name: str(b, "contactDetails.firstName") || "Client",
|
|
126
|
+
service: str(b, "bookedEntity.title", "bookedEntity.slot.serviceName") || "Appointment",
|
|
127
|
+
startIso,
|
|
128
|
+
endIso,
|
|
129
|
+
timeLabel: hhmm(start),
|
|
130
|
+
rangeLabel: end ? `${hhmm(start)} – ${hhmm(end)}` : hhmm(start),
|
|
131
|
+
countdown: countdown(start.getTime(), nowMs),
|
|
132
|
+
whenLabel: whenLabel(start, new Date(nowMs)),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
137
|
+
|
|
138
|
+
/** The day a booking falls on, named as an owner would say it. */
|
|
139
|
+
export function whenLabel(start: Date, now: Date): string {
|
|
140
|
+
if (sameLocalDay(start, now)) return "Today";
|
|
141
|
+
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
|
142
|
+
if (sameLocalDay(start, tomorrow)) return "Tomorrow";
|
|
143
|
+
return `${WEEKDAYS[start.getDay()]} ${start.getDate()} ${MONTHS[start.getMonth()]}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function request(b: Rec): WristRequest {
|
|
147
|
+
const r = row(b);
|
|
148
|
+
const start = new Date(r.startIso);
|
|
149
|
+
const durationMin = r.endIso ? Math.max(0, Math.round((Date.parse(r.endIso) - start.getTime()) / 60000)) : 0;
|
|
150
|
+
return { ...r, revision: str(b, "revision"), durationMin, dayLabel: `${WEEKDAYS[start.getDay()]} ${hhmm(start)}` };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const EMPTY_ROW: WristRow = { bookingId: "", name: "", service: "", startIso: "", endIso: "", timeLabel: "", rangeLabel: "", countdown: "", whenLabel: "" };
|
|
154
|
+
const EMPTY_REQUEST: WristRequest = { ...EMPTY_ROW, revision: "", durationMin: 0, dayLabel: "" };
|
|
155
|
+
|
|
156
|
+
/** Pure: shape a bookings page into the wrist state. `now` is injectable for tests. */
|
|
157
|
+
export function shape(bookings: Rec[], now: Date, site: { id: string; name: string }, requestIndex = 0): WristState {
|
|
158
|
+
const upcoming = bookings
|
|
159
|
+
.filter((b) => str(b, "id") && Date.parse(str(b, "startDate", "bookedEntity.slot.startDate")) >= now.getTime())
|
|
160
|
+
.sort((a, b) => Date.parse(str(a, "startDate")) - Date.parse(str(b, "startDate")));
|
|
161
|
+
const confirmed = upcoming.filter((b) => str(b, "status") === "CONFIRMED");
|
|
162
|
+
const today = confirmed.filter((b) => sameLocalDay(new Date(str(b, "startDate")), now)).map((b) => row(b, now.getTime()));
|
|
163
|
+
// A request this worker just confirmed/declined can still come back PENDING at the same
|
|
164
|
+
// revision from a read that lagged the write; it is not waiting on the owner any more.
|
|
165
|
+
const requests = upcoming
|
|
166
|
+
.filter((b) => PENDING.has(str(b, "status")) && handled.get(str(b, "id")) !== str(b, "revision"))
|
|
167
|
+
.map(request);
|
|
168
|
+
const next = today[0];
|
|
169
|
+
const after = next ? undefined : confirmed.map((b) => row(b, now.getTime()))[0];
|
|
170
|
+
const index = requests.length ? Math.min(Math.max(0, requestIndex), requests.length - 1) : 0;
|
|
171
|
+
const current = requests[index];
|
|
172
|
+
return {
|
|
173
|
+
status: "ready",
|
|
174
|
+
siteId: site.id,
|
|
175
|
+
siteName: site.name,
|
|
176
|
+
nextUp: next ?? EMPTY_ROW,
|
|
177
|
+
hasNextUp: !!next,
|
|
178
|
+
later: today.slice(1),
|
|
179
|
+
requests,
|
|
180
|
+
requestCount: requests.length,
|
|
181
|
+
requestBadge: requests.length === 0 ? "" : `${requests.length} ${requests.length === 1 ? "request" : "requests"}`,
|
|
182
|
+
hasRequests: requests.length > 0,
|
|
183
|
+
noRequests: requests.length === 0,
|
|
184
|
+
hideNoRequests: requests.length > 0,
|
|
185
|
+
requestIndex: index,
|
|
186
|
+
requestPosition: current ? `Request ${index + 1} of ${requests.length}` : "",
|
|
187
|
+
current: current ?? EMPTY_REQUEST,
|
|
188
|
+
doneToday: !next,
|
|
189
|
+
tomorrowFirst: after ?? EMPTY_ROW,
|
|
190
|
+
hasTomorrowFirst: !!after,
|
|
191
|
+
hideTomorrow: !!next || !after,
|
|
192
|
+
inFlight: false,
|
|
193
|
+
confirmingDecline: false,
|
|
194
|
+
notConfirmingDecline: true,
|
|
195
|
+
actionError: "",
|
|
196
|
+
message: next ? "" : COPY.doneToday,
|
|
197
|
+
statusMessage: "",
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
type Host = typeof globalThis & { himalaya?: { input?: Record<string, unknown> } };
|
|
202
|
+
|
|
203
|
+
/** The business this worker's Wix bridge is scoped to. The phone's watch brain passes it as
|
|
204
|
+
* `ownerWristSite` — the same value it checks every Accept/Decline against — so the two can
|
|
205
|
+
* never disagree (the picker's KV entry is written only by the Sites list, not by a push or
|
|
206
|
+
* deep link that also switches business). The bridge-injected business and the picker entry
|
|
207
|
+
* are fallbacks for a host that does not pass it. */
|
|
208
|
+
function site(): { id: string; name: string } {
|
|
209
|
+
const input = (globalThis as Host).himalaya?.input ?? {};
|
|
210
|
+
const pick = (v: unknown) => (typeof v === "string" && v !== "default" ? v.trim() : "");
|
|
211
|
+
const picked = readSelectedSite();
|
|
212
|
+
const id = pick(input.ownerWristSite) || pick(input.__himiPreferenceBusiness) || picked?.id || "";
|
|
213
|
+
return { id, name: picked && picked.id === id ? selectedSiteName("") : "" };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function initial(): WristState {
|
|
217
|
+
return { ...shape([], new Date(), site()), status: "loading", message: "" };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** A failed read shows NOTHING of the last one: after a sign-out or a site switch those rows
|
|
221
|
+
* are someone else's clients. Only the site header survives. */
|
|
222
|
+
/** No usable owner session behind the bridge. On a phone that reads as the bridge refusing
|
|
223
|
+
* ("wixApi not enabled — …") or a 401/403; the web preview's 501 bridge shape counts too. A
|
|
224
|
+
* transient 5xx from Wix is NOT a sign-out — it is an ordinary error. */
|
|
225
|
+
export function isSignedOut(e: unknown): boolean {
|
|
226
|
+
const text = String((e as Error)?.message ?? e);
|
|
227
|
+
if (/wixApi not enabled|session unavailable|not signed in|\bHTTP 40[13]\b|webwix_bridge/i.test(text)) return true;
|
|
228
|
+
return isBridgeUnavailable(e) && /\bHTTP 501\b/.test(text);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function failure(s: WristState, e: unknown): WristState {
|
|
232
|
+
// A degrade screen is ONE line: no "Next up" card, no "No requests waiting" under it.
|
|
233
|
+
const empty = { ...shape([], new Date(), { id: s.siteId, name: s.siteName }), message: "", hideNoRequests: true, hideTomorrow: true };
|
|
234
|
+
if (isSignedOut(e)) return { ...empty, status: "signedOut", message: COPY.signedOut, statusMessage: COPY.signedOut };
|
|
235
|
+
const text = String((e as Error)?.message ?? e);
|
|
236
|
+
// The Bookings app is not installed on the site: its API answers 428 (or 404 for the route).
|
|
237
|
+
if (/\b428\b|\b404\b|not installed|FAILED_PRECONDITION/i.test(text)) {
|
|
238
|
+
return { ...empty, status: "noBookings", message: COPY.noBookings, statusMessage: COPY.noBookings };
|
|
239
|
+
}
|
|
240
|
+
return { ...empty, status: "error", message: COPY.error, statusMessage: COPY.error };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function fetchInto(s: WristState): Promise<WristState> {
|
|
244
|
+
const nowIso = new Date().toISOString();
|
|
245
|
+
const sort = [{ fieldName: "startDate", order: "ASC" }];
|
|
246
|
+
// TWO reads: the day's schedule, and the pending requests on their own. One page of every
|
|
247
|
+
// status would fill with a busy week's confirmed bookings and hide next week's request.
|
|
248
|
+
const [schedule, pending] = await Promise.all([
|
|
249
|
+
queryBookings({ query: { filter: { startDate: { $gte: nowIso }, status: { $nin: ["CANCELED", "DECLINED"] } }, sort, paging: { limit: LIMIT, offset: 0 } } }),
|
|
250
|
+
queryBookings({ query: { filter: { startDate: { $gte: nowIso }, status: { $in: [...PENDING] } }, sort, paging: { limit: LIMIT, offset: 0 } } }),
|
|
251
|
+
]);
|
|
252
|
+
const byId = new Map<string, Rec>();
|
|
253
|
+
for (const b of [...(schedule.bookings ?? []), ...(pending.bookings ?? [])] as unknown as Rec[]) {
|
|
254
|
+
const id = str(b, "id");
|
|
255
|
+
if (id) byId.set(id, b);
|
|
256
|
+
}
|
|
257
|
+
return shape([...byId.values()], new Date(), site(), s.requestIndex);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function isStale(e: unknown): boolean {
|
|
261
|
+
return /revision|\b409\b|ABORTED|conflict/i.test(String((e as Error)?.message ?? e));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
type Write = (bookingId: string, revision: string) => Promise<unknown>;
|
|
265
|
+
|
|
266
|
+
async function respond(s: WristState, params: unknown, write: Write): Promise<WristState> {
|
|
267
|
+
if (s.inFlight) return s;
|
|
268
|
+
const p = (params && typeof params === "object" ? params : {}) as Rec;
|
|
269
|
+
const bookingId = str(p, "bookingId");
|
|
270
|
+
const siteId = str(p, "siteId");
|
|
271
|
+
// A press rendered for another business, or for a request that is no longer pending.
|
|
272
|
+
if (siteId !== site().id) return { ...s, actionError: COPY.changed };
|
|
273
|
+
const target = s.requests.find((r) => r.bookingId === bookingId);
|
|
274
|
+
if (!target) return { ...s, actionError: COPY.changed };
|
|
275
|
+
inFlightNow = true;
|
|
276
|
+
writeGeneration += 1;
|
|
277
|
+
try {
|
|
278
|
+
await write(target.bookingId, target.revision);
|
|
279
|
+
handled.set(target.bookingId, target.revision);
|
|
280
|
+
} catch (e) {
|
|
281
|
+
inFlightNow = false;
|
|
282
|
+
if (isStale(e)) {
|
|
283
|
+
try { return (lastWrite = { ...(await fetchInto(s)), actionError: COPY.changed }); }
|
|
284
|
+
catch { return (lastWrite = { ...s, actionError: COPY.changed }); }
|
|
285
|
+
}
|
|
286
|
+
// Not written. A lost session is a sign-out; anything else keeps the request on screen
|
|
287
|
+
// with an error under its buttons, rather than emptying the queue as if it had worked.
|
|
288
|
+
return (lastWrite = isSignedOut(e) ? failure(s, e) : { ...s, actionError: COPY.writeFailed });
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
return (lastWrite = await fetchInto(s));
|
|
292
|
+
} catch (e) {
|
|
293
|
+
return (lastWrite = failure(s, e));
|
|
294
|
+
} finally {
|
|
295
|
+
inFlightNow = false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Requests this worker wrote, with the revision it wrote against (see `shape`). */
|
|
300
|
+
const handled = new Map<string, string>();
|
|
301
|
+
|
|
302
|
+
// The runtime commits whichever overlapping action finishes LAST, with the state each was
|
|
303
|
+
// dispatched with. A refresh that started before a confirm and lands after it would put the
|
|
304
|
+
// accepted request back on the wrist, so a refresh overtaken by a write returns that write's
|
|
305
|
+
// result instead of its own.
|
|
306
|
+
let writeGeneration = 0;
|
|
307
|
+
let lastWrite: WristState | null = null;
|
|
308
|
+
|
|
309
|
+
async function read(s: WristState): Promise<WristState> {
|
|
310
|
+
const generation = writeGeneration;
|
|
311
|
+
try {
|
|
312
|
+
const next = await fetchInto(s);
|
|
313
|
+
return generation !== writeGeneration || inFlightNow ? (lastWrite ?? s) : next;
|
|
314
|
+
} catch (e) {
|
|
315
|
+
return generation !== writeGeneration || inFlightNow ? (lastWrite ?? s) : failure(s, e);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// The state a concurrent action sees is the one it was dispatched with, so a double tap
|
|
320
|
+
// would read `inFlight: false` twice; this module flag is the guard that actually holds.
|
|
321
|
+
let inFlightNow = false;
|
|
322
|
+
|
|
323
|
+
const svc = defineService<WristState, {
|
|
324
|
+
load: (s: WristState) => Promise<WristState>;
|
|
325
|
+
refresh: (s: WristState) => Promise<WristState>;
|
|
326
|
+
confirm: (s: WristState, params?: unknown) => Promise<WristState>;
|
|
327
|
+
decline: (s: WristState, params?: unknown) => Promise<WristState>;
|
|
328
|
+
nextRequest: (s: WristState) => Promise<WristState>;
|
|
329
|
+
askDecline: (s: WristState) => Promise<WristState>;
|
|
330
|
+
cancelDecline: (s: WristState) => Promise<WristState>;
|
|
331
|
+
}>("ownerWrist", {
|
|
332
|
+
state: initial(),
|
|
333
|
+
actions: {
|
|
334
|
+
load: async (s) => read(s),
|
|
335
|
+
refresh: async (s) => read(s),
|
|
336
|
+
confirm: async (s, params) => (inFlightNow ? s : respond(s, params, (id, rev) => confirmBooking(id, rev))),
|
|
337
|
+
decline: async (s, params) => (inFlightNow ? s : respond(s, params, (id, rev) => declineBooking(id, rev))),
|
|
338
|
+
askDecline: async (s) => (s.current.bookingId ? { ...s, confirmingDecline: true, notConfirmingDecline: false, actionError: "" } : s),
|
|
339
|
+
cancelDecline: async (s) => ({ ...s, confirmingDecline: false, notConfirmingDecline: true }),
|
|
340
|
+
nextRequest: async (s) => {
|
|
341
|
+
if (!s.requests.length) return s;
|
|
342
|
+
const index = (s.requestIndex + 1) % s.requests.length;
|
|
343
|
+
const current = s.requests[index];
|
|
344
|
+
return { ...s, requestIndex: index, current, requestPosition: `Request ${index + 1} of ${s.requests.length}`, actionError: "" };
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
svc.load();
|
|
350
|
+
export { svc };
|
|
@@ -2841,6 +2841,93 @@ const SCREENS = runtimeSetting({
|
|
|
2841
2841
|
"accessibilityLabel": "Saved posts"
|
|
2842
2842
|
}
|
|
2843
2843
|
},
|
|
2844
|
+
{
|
|
2845
|
+
"id": "himi_account.restore",
|
|
2846
|
+
"body": {
|
|
2847
|
+
"package": "himalaya",
|
|
2848
|
+
"type": "HStack",
|
|
2849
|
+
"props": {
|
|
2850
|
+
"spacing": 12,
|
|
2851
|
+
"children": [
|
|
2852
|
+
{
|
|
2853
|
+
"id": "",
|
|
2854
|
+
"body": {
|
|
2855
|
+
"package": "himalaya",
|
|
2856
|
+
"type": "Icon",
|
|
2857
|
+
"props": {
|
|
2858
|
+
"name": "refresh",
|
|
2859
|
+
"size": 18,
|
|
2860
|
+
"tint": "accentPrimary"
|
|
2861
|
+
}
|
|
2862
|
+
},
|
|
2863
|
+
"modifiers": {
|
|
2864
|
+
"accessibilityRole": "decorative"
|
|
2865
|
+
}
|
|
2866
|
+
},
|
|
2867
|
+
{
|
|
2868
|
+
"id": "",
|
|
2869
|
+
"body": {
|
|
2870
|
+
"package": "himalaya",
|
|
2871
|
+
"type": "Text",
|
|
2872
|
+
"props": {
|
|
2873
|
+
"text": "$service:himiStoreAccount.restoreLabel",
|
|
2874
|
+
"style": "bodyEmphasis"
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
},
|
|
2878
|
+
{
|
|
2879
|
+
"id": "",
|
|
2880
|
+
"body": {
|
|
2881
|
+
"package": "himalaya",
|
|
2882
|
+
"type": "Spacer",
|
|
2883
|
+
"props": {}
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
]
|
|
2887
|
+
}
|
|
2888
|
+
},
|
|
2889
|
+
"modifiers": {
|
|
2890
|
+
"background": {
|
|
2891
|
+
"token": "surfaceElevated"
|
|
2892
|
+
},
|
|
2893
|
+
"border": {
|
|
2894
|
+
"width": 1,
|
|
2895
|
+
"color": {
|
|
2896
|
+
"token": "border"
|
|
2897
|
+
}
|
|
2898
|
+
},
|
|
2899
|
+
"cornerRadius": 8,
|
|
2900
|
+
"padding": {
|
|
2901
|
+
"leading": 14,
|
|
2902
|
+
"trailing": 14,
|
|
2903
|
+
"top": 13,
|
|
2904
|
+
"bottom": 13
|
|
2905
|
+
},
|
|
2906
|
+
"onTap": {
|
|
2907
|
+
"actionId": "tier5.himiStoreAccount.iapRestore"
|
|
2908
|
+
},
|
|
2909
|
+
"pressFeedback": "scale",
|
|
2910
|
+
"haptic": "light",
|
|
2911
|
+
"accessibilityLabel": "$service:himiStoreAccount.restoreLabel",
|
|
2912
|
+
"hiddenPath": "$service:himiStoreAccount.restoreHidden"
|
|
2913
|
+
}
|
|
2914
|
+
},
|
|
2915
|
+
{
|
|
2916
|
+
"id": "himi_account.restore_message",
|
|
2917
|
+
"body": {
|
|
2918
|
+
"package": "himalaya",
|
|
2919
|
+
"type": "Text",
|
|
2920
|
+
"props": {
|
|
2921
|
+
"text": "$service:himiStoreAccount.restoreMessage",
|
|
2922
|
+
"style": "footnote",
|
|
2923
|
+
"color": "textSecondary",
|
|
2924
|
+
"collapseWhenEmpty": true
|
|
2925
|
+
}
|
|
2926
|
+
},
|
|
2927
|
+
"modifiers": {
|
|
2928
|
+
"hiddenPath": "$service:himiStoreAccount.restoreHidden"
|
|
2929
|
+
}
|
|
2930
|
+
},
|
|
2844
2931
|
{
|
|
2845
2932
|
"id": "himi_account.view_profile",
|
|
2846
2933
|
"body": {
|
|
@@ -3003,6 +3090,109 @@ const SCREENS = runtimeSetting({
|
|
|
3003
3090
|
"hiddenPath": "$service:himiStoreAccount.notMember"
|
|
3004
3091
|
}
|
|
3005
3092
|
},
|
|
3093
|
+
{
|
|
3094
|
+
"id": "",
|
|
3095
|
+
"body": {
|
|
3096
|
+
"package": "himalaya",
|
|
3097
|
+
"type": "VStack",
|
|
3098
|
+
"props": {
|
|
3099
|
+
"spacing": 8,
|
|
3100
|
+
"alignment": "leading",
|
|
3101
|
+
"children": [
|
|
3102
|
+
{
|
|
3103
|
+
"id": "himi_account.restore_guest",
|
|
3104
|
+
"body": {
|
|
3105
|
+
"package": "himalaya",
|
|
3106
|
+
"type": "HStack",
|
|
3107
|
+
"props": {
|
|
3108
|
+
"spacing": 12,
|
|
3109
|
+
"children": [
|
|
3110
|
+
{
|
|
3111
|
+
"id": "",
|
|
3112
|
+
"body": {
|
|
3113
|
+
"package": "himalaya",
|
|
3114
|
+
"type": "Icon",
|
|
3115
|
+
"props": {
|
|
3116
|
+
"name": "refresh",
|
|
3117
|
+
"size": 18,
|
|
3118
|
+
"tint": "accentPrimary"
|
|
3119
|
+
}
|
|
3120
|
+
},
|
|
3121
|
+
"modifiers": {
|
|
3122
|
+
"accessibilityRole": "decorative"
|
|
3123
|
+
}
|
|
3124
|
+
},
|
|
3125
|
+
{
|
|
3126
|
+
"id": "",
|
|
3127
|
+
"body": {
|
|
3128
|
+
"package": "himalaya",
|
|
3129
|
+
"type": "Text",
|
|
3130
|
+
"props": {
|
|
3131
|
+
"text": "$service:himiStoreAccount.restoreLabel",
|
|
3132
|
+
"style": "bodyEmphasis"
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
},
|
|
3136
|
+
{
|
|
3137
|
+
"id": "",
|
|
3138
|
+
"body": {
|
|
3139
|
+
"package": "himalaya",
|
|
3140
|
+
"type": "Spacer",
|
|
3141
|
+
"props": {}
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
]
|
|
3145
|
+
}
|
|
3146
|
+
},
|
|
3147
|
+
"modifiers": {
|
|
3148
|
+
"background": {
|
|
3149
|
+
"token": "surfaceElevated"
|
|
3150
|
+
},
|
|
3151
|
+
"border": {
|
|
3152
|
+
"width": 1,
|
|
3153
|
+
"color": {
|
|
3154
|
+
"token": "border"
|
|
3155
|
+
}
|
|
3156
|
+
},
|
|
3157
|
+
"cornerRadius": 8,
|
|
3158
|
+
"padding": {
|
|
3159
|
+
"leading": 14,
|
|
3160
|
+
"trailing": 14,
|
|
3161
|
+
"top": 13,
|
|
3162
|
+
"bottom": 13
|
|
3163
|
+
},
|
|
3164
|
+
"onTap": {
|
|
3165
|
+
"actionId": "tier5.himiStoreAccount.iapRestore"
|
|
3166
|
+
},
|
|
3167
|
+
"pressFeedback": "scale",
|
|
3168
|
+
"haptic": "light",
|
|
3169
|
+
"accessibilityLabel": "$service:himiStoreAccount.restoreLabel",
|
|
3170
|
+
"hiddenPath": "$service:himiStoreAccount.restoreHidden"
|
|
3171
|
+
}
|
|
3172
|
+
},
|
|
3173
|
+
{
|
|
3174
|
+
"id": "himi_account.restore_guest_message",
|
|
3175
|
+
"body": {
|
|
3176
|
+
"package": "himalaya",
|
|
3177
|
+
"type": "Text",
|
|
3178
|
+
"props": {
|
|
3179
|
+
"text": "$service:himiStoreAccount.restoreMessage",
|
|
3180
|
+
"style": "footnote",
|
|
3181
|
+
"color": "textSecondary",
|
|
3182
|
+
"collapseWhenEmpty": true
|
|
3183
|
+
}
|
|
3184
|
+
},
|
|
3185
|
+
"modifiers": {
|
|
3186
|
+
"hiddenPath": "$service:himiStoreAccount.restoreHidden"
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
]
|
|
3190
|
+
}
|
|
3191
|
+
},
|
|
3192
|
+
"modifiers": {
|
|
3193
|
+
"hiddenPath": "$service:himiStoreAccount.isMember"
|
|
3194
|
+
}
|
|
3195
|
+
},
|
|
3006
3196
|
{
|
|
3007
3197
|
"id": "himi_account.language",
|
|
3008
3198
|
"body": {
|
|
@@ -3372,6 +3562,7 @@ const SCREENS = runtimeSetting({
|
|
|
3372
3562
|
"actionId": "tier5.himiStoreAccount.refresh",
|
|
3373
3563
|
"params": {
|
|
3374
3564
|
"memberAccountLinks": "1",
|
|
3565
|
+
"restorePurchases": "1",
|
|
3375
3566
|
"copy": "{\"account.subtitle\":\"RSVPs, groups and saved content in one place.\",\"storeAccount.authBody\":\"Sign in to continue — RSVPs and groups stay with you.\",\"storeAccount.authBodyRegister\":\"Create a member account to continue — RSVPs and groups stay with you.\"}"
|
|
3376
3567
|
}
|
|
3377
3568
|
}
|
|
@@ -3383,7 +3574,8 @@ const SCREENS = runtimeSetting({
|
|
|
3383
3574
|
"contentType": "application/javascript+ts-bundle",
|
|
3384
3575
|
"declaredCapabilities": [
|
|
3385
3576
|
"wix_api",
|
|
3386
|
-
"app_info"
|
|
3577
|
+
"app_info",
|
|
3578
|
+
"iap"
|
|
3387
3579
|
],
|
|
3388
3580
|
"minBinaryVersion": "0.2.0",
|
|
3389
3581
|
"bundleVersion": "0.0.1"
|
|
@@ -98,7 +98,7 @@ export const COLLECTIONS: Record<string, CollectionConfig> = {
|
|
|
98
98
|
himiBrandedBadges: { rows: 'badges', title: 'name', subtitle: 'description', view: 'grid' },
|
|
99
99
|
himiBrandedDonations: { rows: 'campaigns', title: 'name', subtitle: 'description', view: 'grid', primaryVerb: { label: 'Donate' } },
|
|
100
100
|
himiBrandedGiftCards: { rows: 'cards', title: 'name', subtitle: 'description', view: 'grid' },
|
|
101
|
-
himiBrandedVideos: { rows: 'channels', title: 'title', subtitle: '
|
|
101
|
+
himiBrandedVideos: { rows: 'channels', title: 'title', subtitle: 'subtitle', image: 'coverUrl', view: 'grid', searchPlaceholder: 'Search channels', countNoun: ['channel', 'channels'] },
|
|
102
102
|
himiBrandedPodcasts: { rows: 'episodes', title: 'title', subtitle: 'subtitle', image: 'image', metadata: ['progressLabel'], view: 'list' },
|
|
103
103
|
himiBrandedForms: { rows: 'forms', title: 'name', subtitle: 'description', view: 'list' },
|
|
104
104
|
himiBrandedGalleries: { rows: 'galleries', title: 'name', subtitle: 'description', image: 'image', view: 'grid' },
|