@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.
Files changed (149) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/commerce-contract.d.ts +24 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/commerce-core.d.ts +3 -0
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/index.d.ts +1 -1
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/video/src/display.d.ts +66 -0
  5. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/video/src/index.d.ts +185 -0
  6. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +57 -0
  7. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +57 -0
  8. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-video.mjs +324 -0
  9. package/dist/authoring-runtime/node_modules/@wix/himalaya/package.json +4 -0
  10. package/dist/cli.mjs +10 -5
  11. package/guides/iap-screen-adoption.md +53 -1
  12. package/guides/importable-sdks.md +1 -0
  13. package/guides/sdk-reference.json +261 -2
  14. package/package.json +2 -2
  15. package/templates/beauty-salon/dev/index.ts +193 -1
  16. package/templates/beauty-salon/tier5-src/_shared/collection-contract.ts +1 -1
  17. package/templates/beauty-salon/tier5-src/_shared/iap-worker.ts +84 -3
  18. package/templates/beauty-salon/tier5-src/_shared/video-caption.ts +21 -0
  19. package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +31 -1
  20. package/templates/beauty-salon/tier5-src/himi-store-account/index.ts +36 -1
  21. package/templates/beauty-salon-owner/config.ts +11 -0
  22. package/templates/beauty-salon-owner/dev/index.ts +540 -0
  23. package/templates/beauty-salon-owner/himalaya.content.json +6 -0
  24. package/templates/beauty-salon-owner/platforms.json +2 -1
  25. package/templates/beauty-salon-owner/tier5-src/_shared/collection-contract.ts +1 -1
  26. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
  27. package/templates/branded-lite/dev/index.ts +1212 -54
  28. package/templates/branded-lite/tier5-src/_shared/collection-contract.ts +1 -1
  29. package/templates/branded-lite/tier5-src/_shared/iap-worker.ts +84 -3
  30. package/templates/branded-lite/tier5-src/_shared/video-caption.ts +21 -0
  31. package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +31 -1
  32. package/templates/branded-lite/tier5-src/himi-branded-lite-video/index.ts +23 -32
  33. package/templates/branded-lite/tier5-src/himi-branded-lite-videos/index.ts +44 -38
  34. package/templates/branded-lite/tier5-src/himi-store-account/index.ts +36 -1
  35. package/templates/community-club/dev/index.ts +193 -1
  36. package/templates/community-club/tier5-src/_shared/collection-contract.ts +1 -1
  37. package/templates/community-club/tier5-src/_shared/iap-worker.ts +84 -3
  38. package/templates/community-club/tier5-src/_shared/video-caption.ts +21 -0
  39. package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +31 -1
  40. package/templates/community-club/tier5-src/himi-store-account/index.ts +36 -1
  41. package/templates/community-club-owner/config.ts +11 -0
  42. package/templates/community-club-owner/dev/index.ts +540 -0
  43. package/templates/community-club-owner/himalaya.content.json +6 -0
  44. package/templates/community-club-owner/platforms.json +2 -1
  45. package/templates/community-club-owner/tier5-src/_shared/collection-contract.ts +1 -1
  46. package/templates/community-club-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
  47. package/templates/events-meetups/dev/index.ts +193 -1
  48. package/templates/events-meetups/tier5-src/_shared/collection-contract.ts +1 -1
  49. package/templates/events-meetups/tier5-src/_shared/iap-worker.ts +84 -3
  50. package/templates/events-meetups/tier5-src/_shared/video-caption.ts +21 -0
  51. package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +31 -1
  52. package/templates/events-meetups/tier5-src/himi-store-account/index.ts +36 -1
  53. package/templates/events-meetups-owner/config.ts +11 -0
  54. package/templates/events-meetups-owner/dev/index.ts +540 -0
  55. package/templates/events-meetups-owner/himalaya.content.json +6 -0
  56. package/templates/events-meetups-owner/platforms.json +2 -1
  57. package/templates/events-meetups-owner/tier5-src/_shared/collection-contract.ts +1 -1
  58. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
  59. package/templates/gym-fitness/dev/index.ts +193 -1
  60. package/templates/gym-fitness/tier5-src/_shared/collection-contract.ts +1 -1
  61. package/templates/gym-fitness/tier5-src/_shared/iap-worker.ts +84 -3
  62. package/templates/gym-fitness/tier5-src/_shared/video-caption.ts +21 -0
  63. package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +31 -1
  64. package/templates/gym-fitness/tier5-src/himi-store-account/index.ts +36 -1
  65. package/templates/gym-fitness-owner/config.ts +11 -0
  66. package/templates/gym-fitness-owner/dev/index.ts +540 -0
  67. package/templates/gym-fitness-owner/himalaya.content.json +6 -0
  68. package/templates/gym-fitness-owner/platforms.json +2 -1
  69. package/templates/gym-fitness-owner/tier5-src/_shared/collection-contract.ts +1 -1
  70. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
  71. package/templates/nonprofit-cause/dev/index.ts +193 -1
  72. package/templates/nonprofit-cause/tier5-src/_shared/collection-contract.ts +1 -1
  73. package/templates/nonprofit-cause/tier5-src/_shared/iap-worker.ts +84 -3
  74. package/templates/nonprofit-cause/tier5-src/_shared/video-caption.ts +21 -0
  75. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +31 -1
  76. package/templates/nonprofit-cause/tier5-src/himi-store-account/index.ts +36 -1
  77. package/templates/nonprofit-cause-owner/config.ts +11 -0
  78. package/templates/nonprofit-cause-owner/dev/index.ts +540 -0
  79. package/templates/nonprofit-cause-owner/himalaya.content.json +6 -0
  80. package/templates/nonprofit-cause-owner/platforms.json +2 -1
  81. package/templates/nonprofit-cause-owner/tier5-src/_shared/collection-contract.ts +1 -1
  82. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
  83. package/templates/owner-lite/config.ts +11 -0
  84. package/templates/owner-lite/dev/index.ts +540 -0
  85. package/templates/owner-lite/himalaya.content.json +6 -0
  86. package/templates/owner-lite/platforms.json +2 -1
  87. package/templates/owner-lite/tier5-src/_shared/collection-contract.ts +1 -1
  88. package/templates/owner-lite/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
  89. package/templates/podcast-creator/dev/index.ts +193 -1
  90. package/templates/podcast-creator/tier5-src/_shared/collection-contract.ts +1 -1
  91. package/templates/podcast-creator/tier5-src/_shared/iap-worker.ts +84 -3
  92. package/templates/podcast-creator/tier5-src/_shared/video-caption.ts +21 -0
  93. package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +31 -1
  94. package/templates/podcast-creator/tier5-src/himi-store-account/index.ts +36 -1
  95. package/templates/podcast-creator-owner/config.ts +11 -0
  96. package/templates/podcast-creator-owner/dev/index.ts +540 -0
  97. package/templates/podcast-creator-owner/himalaya.content.json +6 -0
  98. package/templates/podcast-creator-owner/platforms.json +2 -1
  99. package/templates/podcast-creator-owner/tier5-src/_shared/collection-contract.ts +1 -1
  100. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
  101. package/templates/portfolio-creative/dev/index.ts +193 -1
  102. package/templates/portfolio-creative/tier5-src/_shared/collection-contract.ts +1 -1
  103. package/templates/portfolio-creative/tier5-src/_shared/iap-worker.ts +84 -3
  104. package/templates/portfolio-creative/tier5-src/_shared/video-caption.ts +21 -0
  105. package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +31 -1
  106. package/templates/portfolio-creative/tier5-src/himi-store-account/index.ts +36 -1
  107. package/templates/portfolio-creative-owner/config.ts +11 -0
  108. package/templates/portfolio-creative-owner/dev/index.ts +540 -0
  109. package/templates/portfolio-creative-owner/himalaya.content.json +6 -0
  110. package/templates/portfolio-creative-owner/platforms.json +2 -1
  111. package/templates/portfolio-creative-owner/tier5-src/_shared/collection-contract.ts +1 -1
  112. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
  113. package/templates/restaurant/dev/index.ts +193 -1
  114. package/templates/restaurant/tier5-src/_shared/collection-contract.ts +1 -1
  115. package/templates/restaurant/tier5-src/_shared/iap-worker.ts +84 -3
  116. package/templates/restaurant/tier5-src/_shared/video-caption.ts +21 -0
  117. package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +31 -1
  118. package/templates/restaurant/tier5-src/himi-store-account/index.ts +36 -1
  119. package/templates/restaurant-owner/config.ts +11 -0
  120. package/templates/restaurant-owner/dev/index.ts +540 -0
  121. package/templates/restaurant-owner/himalaya.content.json +6 -0
  122. package/templates/restaurant-owner/platforms.json +2 -1
  123. package/templates/restaurant-owner/tier5-src/_shared/collection-contract.ts +1 -1
  124. package/templates/restaurant-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
  125. package/templates/retail-store/dev/index.ts +193 -1
  126. package/templates/retail-store/tier5-src/_shared/collection-contract.ts +1 -1
  127. package/templates/retail-store/tier5-src/_shared/iap-worker.ts +84 -3
  128. package/templates/retail-store/tier5-src/_shared/video-caption.ts +21 -0
  129. package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +31 -1
  130. package/templates/retail-store/tier5-src/himi-store-account/index.ts +36 -1
  131. package/templates/retail-store-owner/config.ts +11 -0
  132. package/templates/retail-store-owner/dev/index.ts +540 -0
  133. package/templates/retail-store-owner/himalaya.content.json +6 -0
  134. package/templates/retail-store-owner/platforms.json +2 -1
  135. package/templates/retail-store-owner/tier5-src/_shared/collection-contract.ts +1 -1
  136. package/templates/retail-store-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
  137. package/templates/yoga-studio/dev/index.ts +193 -1
  138. package/templates/yoga-studio/tier5-src/_shared/collection-contract.ts +1 -1
  139. package/templates/yoga-studio/tier5-src/_shared/iap-worker.ts +84 -3
  140. package/templates/yoga-studio/tier5-src/_shared/video-caption.ts +21 -0
  141. package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +31 -1
  142. package/templates/yoga-studio/tier5-src/himi-store-account/index.ts +36 -1
  143. package/templates/yoga-studio-owner/config.ts +11 -0
  144. package/templates/yoga-studio-owner/dev/index.ts +540 -0
  145. package/templates/yoga-studio-owner/himalaya.content.json +6 -0
  146. package/templates/yoga-studio-owner/platforms.json +2 -1
  147. package/templates/yoga-studio-owner/tier5-src/_shared/collection-contract.ts +1 -1
  148. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-wrist/index.ts +350 -0
  149. 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 };
@@ -3227,6 +3227,93 @@ const SCREENS = runtimeSetting({
3227
3227
  "accessibilityLabel": "My subscriptions"
3228
3228
  }
3229
3229
  },
3230
+ {
3231
+ "id": "himi_account.restore",
3232
+ "body": {
3233
+ "package": "himalaya",
3234
+ "type": "HStack",
3235
+ "props": {
3236
+ "spacing": 12,
3237
+ "children": [
3238
+ {
3239
+ "id": "",
3240
+ "body": {
3241
+ "package": "himalaya",
3242
+ "type": "Icon",
3243
+ "props": {
3244
+ "name": "refresh",
3245
+ "size": 18,
3246
+ "tint": "accentPrimary"
3247
+ }
3248
+ },
3249
+ "modifiers": {
3250
+ "accessibilityRole": "decorative"
3251
+ }
3252
+ },
3253
+ {
3254
+ "id": "",
3255
+ "body": {
3256
+ "package": "himalaya",
3257
+ "type": "Text",
3258
+ "props": {
3259
+ "text": "$service:himiStoreAccount.restoreLabel",
3260
+ "style": "bodyEmphasis"
3261
+ }
3262
+ }
3263
+ },
3264
+ {
3265
+ "id": "",
3266
+ "body": {
3267
+ "package": "himalaya",
3268
+ "type": "Spacer",
3269
+ "props": {}
3270
+ }
3271
+ }
3272
+ ]
3273
+ }
3274
+ },
3275
+ "modifiers": {
3276
+ "background": {
3277
+ "token": "surfaceElevated"
3278
+ },
3279
+ "border": {
3280
+ "width": 1,
3281
+ "color": {
3282
+ "token": "border"
3283
+ }
3284
+ },
3285
+ "cornerRadius": 8,
3286
+ "padding": {
3287
+ "leading": 14,
3288
+ "trailing": 14,
3289
+ "top": 13,
3290
+ "bottom": 13
3291
+ },
3292
+ "onTap": {
3293
+ "actionId": "tier5.himiStoreAccount.iapRestore"
3294
+ },
3295
+ "pressFeedback": "scale",
3296
+ "haptic": "light",
3297
+ "accessibilityLabel": "$service:himiStoreAccount.restoreLabel",
3298
+ "hiddenPath": "$service:himiStoreAccount.restoreHidden"
3299
+ }
3300
+ },
3301
+ {
3302
+ "id": "himi_account.restore_message",
3303
+ "body": {
3304
+ "package": "himalaya",
3305
+ "type": "Text",
3306
+ "props": {
3307
+ "text": "$service:himiStoreAccount.restoreMessage",
3308
+ "style": "footnote",
3309
+ "color": "textSecondary",
3310
+ "collapseWhenEmpty": true
3311
+ }
3312
+ },
3313
+ "modifiers": {
3314
+ "hiddenPath": "$service:himiStoreAccount.restoreHidden"
3315
+ }
3316
+ },
3230
3317
  {
3231
3318
  "id": "himi_account.wallet",
3232
3319
  "body": {
@@ -3567,6 +3654,109 @@ const SCREENS = runtimeSetting({
3567
3654
  "hiddenPath": "$service:himiStoreAccount.isMember"
3568
3655
  }
3569
3656
  },
3657
+ {
3658
+ "id": "",
3659
+ "body": {
3660
+ "package": "himalaya",
3661
+ "type": "VStack",
3662
+ "props": {
3663
+ "spacing": 8,
3664
+ "alignment": "leading",
3665
+ "children": [
3666
+ {
3667
+ "id": "himi_account.restore_guest",
3668
+ "body": {
3669
+ "package": "himalaya",
3670
+ "type": "HStack",
3671
+ "props": {
3672
+ "spacing": 12,
3673
+ "children": [
3674
+ {
3675
+ "id": "",
3676
+ "body": {
3677
+ "package": "himalaya",
3678
+ "type": "Icon",
3679
+ "props": {
3680
+ "name": "refresh",
3681
+ "size": 18,
3682
+ "tint": "accentPrimary"
3683
+ }
3684
+ },
3685
+ "modifiers": {
3686
+ "accessibilityRole": "decorative"
3687
+ }
3688
+ },
3689
+ {
3690
+ "id": "",
3691
+ "body": {
3692
+ "package": "himalaya",
3693
+ "type": "Text",
3694
+ "props": {
3695
+ "text": "$service:himiStoreAccount.restoreLabel",
3696
+ "style": "bodyEmphasis"
3697
+ }
3698
+ }
3699
+ },
3700
+ {
3701
+ "id": "",
3702
+ "body": {
3703
+ "package": "himalaya",
3704
+ "type": "Spacer",
3705
+ "props": {}
3706
+ }
3707
+ }
3708
+ ]
3709
+ }
3710
+ },
3711
+ "modifiers": {
3712
+ "background": {
3713
+ "token": "surfaceElevated"
3714
+ },
3715
+ "border": {
3716
+ "width": 1,
3717
+ "color": {
3718
+ "token": "border"
3719
+ }
3720
+ },
3721
+ "cornerRadius": 8,
3722
+ "padding": {
3723
+ "leading": 14,
3724
+ "trailing": 14,
3725
+ "top": 13,
3726
+ "bottom": 13
3727
+ },
3728
+ "onTap": {
3729
+ "actionId": "tier5.himiStoreAccount.iapRestore"
3730
+ },
3731
+ "pressFeedback": "scale",
3732
+ "haptic": "light",
3733
+ "accessibilityLabel": "$service:himiStoreAccount.restoreLabel",
3734
+ "hiddenPath": "$service:himiStoreAccount.restoreHidden"
3735
+ }
3736
+ },
3737
+ {
3738
+ "id": "himi_account.restore_guest_message",
3739
+ "body": {
3740
+ "package": "himalaya",
3741
+ "type": "Text",
3742
+ "props": {
3743
+ "text": "$service:himiStoreAccount.restoreMessage",
3744
+ "style": "footnote",
3745
+ "color": "textSecondary",
3746
+ "collapseWhenEmpty": true
3747
+ }
3748
+ },
3749
+ "modifiers": {
3750
+ "hiddenPath": "$service:himiStoreAccount.restoreHidden"
3751
+ }
3752
+ }
3753
+ ]
3754
+ }
3755
+ },
3756
+ "modifiers": {
3757
+ "hiddenPath": "$service:himiStoreAccount.isMember"
3758
+ }
3759
+ },
3570
3760
  {
3571
3761
  "id": "himi_account.language",
3572
3762
  "body": {
@@ -3936,6 +4126,7 @@ const SCREENS = runtimeSetting({
3936
4126
  "actionId": "tier5.himiStoreAccount.refresh",
3937
4127
  "params": {
3938
4128
  "memberAccountLinks": "1",
4129
+ "restorePurchases": "1",
3939
4130
  "copy": "{\"account.subtitle\":\"Bookings, RSVPs, memberships, programs and saved content in one place.\",\"storeAccount.authBody\":\"Sign in to continue — bookings, RSVPs, memberships and programs stay with you.\",\"storeAccount.authBodyRegister\":\"Create a member account to continue — bookings, RSVPs, memberships and programs stay with you.\"}"
3940
4131
  }
3941
4132
  }
@@ -3947,7 +4138,8 @@ const SCREENS = runtimeSetting({
3947
4138
  "contentType": "application/javascript+ts-bundle",
3948
4139
  "declaredCapabilities": [
3949
4140
  "wix_api",
3950
- "app_info"
4141
+ "app_info",
4142
+ "iap"
3951
4143
  ],
3952
4144
  "minBinaryVersion": "0.2.0",
3953
4145
  "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: 'description', view: 'grid' },
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' },