@wix/himalaya-cli 0.1560.0 → 0.1561.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/dev-server/src/builder.d.ts +3 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/ts/src/schema.d.ts +2 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/events/src/index.d.ts +60 -7
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-events.mjs +11 -5
- package/dist/cli.mjs +51 -42
- package/dist/preview-assets.generated.bin +0 -0
- package/guides/component-reference.json +3 -2
- package/guides/component-reference.md +3 -1
- package/guides/sdk-reference.json +7 -7
- package/package.json +2 -2
- package/templates/beauty-salon/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/beauty-salon/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/beauty-salon-owner/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/branded-lite/dev/index.ts +2390 -851
- package/templates/branded-lite/himalaya.content.json +1 -1
- package/templates/branded-lite/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/branded-lite/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/branded-lite/tier5-src/himi-events-detail/index.ts +803 -361
- package/templates/community-club/dev/index.ts +2387 -848
- package/templates/community-club/himalaya.content.json +1 -1
- package/templates/community-club/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/community-club/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/community-club/tier5-src/himi-events-detail/index.ts +803 -361
- package/templates/community-club-owner/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/community-club-owner/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/events-meetups/dev/index.ts +2387 -848
- package/templates/events-meetups/himalaya.content.json +1 -1
- package/templates/events-meetups/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/events-meetups/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/events-meetups/tier5-src/himi-events-detail/index.ts +803 -361
- package/templates/events-meetups-owner/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/events-meetups-owner/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/gym-fitness/dev/index.ts +2392 -853
- package/templates/gym-fitness/himalaya.content.json +1 -1
- package/templates/gym-fitness/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/gym-fitness/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/gym-fitness/tier5-src/himi-events-detail/index.ts +803 -361
- package/templates/gym-fitness-owner/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/gym-fitness-owner/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/nonprofit-cause/dev/index.ts +2387 -848
- package/templates/nonprofit-cause/himalaya.content.json +1 -1
- package/templates/nonprofit-cause/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/nonprofit-cause/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/nonprofit-cause/tier5-src/himi-events-detail/index.ts +803 -361
- package/templates/nonprofit-cause-owner/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/nonprofit-cause-owner/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/owner-lite/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/owner-lite/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/podcast-creator/dev/index.ts +2388 -849
- package/templates/podcast-creator/himalaya.content.json +1 -1
- package/templates/podcast-creator/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/podcast-creator/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/podcast-creator/tier5-src/himi-events-detail/index.ts +803 -361
- package/templates/podcast-creator-owner/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/podcast-creator-owner/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/portfolio-creative/dev/index.ts +2375 -836
- package/templates/portfolio-creative/himalaya.content.json +1 -1
- package/templates/portfolio-creative/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/portfolio-creative/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/portfolio-creative/tier5-src/himi-events-detail/index.ts +803 -361
- package/templates/portfolio-creative-owner/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/portfolio-creative-owner/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/restaurant/dev/index.ts +2388 -849
- package/templates/restaurant/himalaya.content.json +1 -1
- package/templates/restaurant/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/restaurant/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/restaurant/tier5-src/himi-events-detail/index.ts +803 -361
- package/templates/restaurant-owner/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/restaurant-owner/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/retail-store/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/retail-store/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/retail-store-owner/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/retail-store-owner/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/yoga-studio/dev/index.ts +2403 -864
- package/templates/yoga-studio/himalaya.content.json +1 -1
- package/templates/yoga-studio/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/yoga-studio/tier5-src/_shared/himi-events.ts +7 -0
- package/templates/yoga-studio/tier5-src/himi-events-detail/index.ts +803 -361
- package/templates/yoga-studio-owner/tier5-src/_shared/engine-intl.ts +18 -0
- package/templates/yoga-studio-owner/tier5-src/_shared/himi-events.ts +7 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Tier-5 bundle: himi-events — event detail.
|
|
2
|
-
import { formatCurrency } from "../_shared/engine-intl.js";
|
|
2
|
+
import { calendarDay, formatClock, formatCurrency, formatWeekdayDateTime, formatWeekdayMonthDay, formatTimeZoneLabel } from "../_shared/engine-intl.js";
|
|
3
3
|
import { defineService as registerService } from "@himalaya/state";
|
|
4
|
+
import { dispatch } from "@himalaya/dispatch";
|
|
4
5
|
import { withOnlineWrites } from "../_shared/online-actions.js";
|
|
6
|
+
import { hostedSitePageOpener, isHostedWorker } from "../_shared/hosted-checkout.js";
|
|
5
7
|
import { createEvent, getPermissionStatus, requestPermission } from "@himalaya/calendar";
|
|
6
8
|
import { open } from "@himalaya/open-url";
|
|
7
9
|
import { share as shareSheet } from "@himalaya/share";
|
|
@@ -10,70 +12,61 @@ import {
|
|
|
10
12
|
checkoutReservation,
|
|
11
13
|
createTicketReservation,
|
|
12
14
|
getEvent,
|
|
13
|
-
getEventGuestList,
|
|
14
15
|
queryAvailableTickets,
|
|
15
|
-
queryEvents,
|
|
16
16
|
type WixAvailableTicket,
|
|
17
17
|
type WixEventFieldset,
|
|
18
18
|
type WixEvent,
|
|
19
|
-
type WixGuestRsvp,
|
|
20
|
-
type WixGuestTicket,
|
|
21
19
|
} from "@himalaya/wix-events";
|
|
22
20
|
import { session as wixPlatformSession } from "@himalaya/wix-platform";
|
|
23
21
|
import { payCashierOrder } from "@himalaya/checkout";
|
|
24
22
|
import { get, upsert, type DbEntity } from "@himalaya/local-db";
|
|
25
23
|
import {
|
|
26
24
|
currentInputId,
|
|
25
|
+
forgetRsvp,
|
|
27
26
|
eventDescription,
|
|
28
|
-
eventEndMillis,
|
|
29
27
|
eventImage,
|
|
30
28
|
eventLocation,
|
|
29
|
+
eventAddress,
|
|
31
30
|
eventMapUrl,
|
|
32
31
|
eventStartMillis,
|
|
33
32
|
eventWhen,
|
|
34
33
|
normalizeEvent,
|
|
35
|
-
titleCase,
|
|
36
34
|
type EventRow,
|
|
37
35
|
} from "../_shared/himi-events";
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
36
|
+
import { stashCopy, copyParam } from "../_shared/himi-copy.js";
|
|
37
|
+
import { openRichLink } from "../_shared/rich-content-link.js";
|
|
38
|
+
import { richBody } from "../_shared/rich-body.js";
|
|
40
39
|
import { mayUseRegularTicketCheckout } from "../_shared/iap-attendance.js";
|
|
41
40
|
import { ATTENDANCE_INITIAL, ATTENDANCE_NOT_HYDRATED, createAttendancePurchase, type AttendancePurchaseFields } from "../_shared/iap-attendance-purchase.js";
|
|
42
41
|
import { resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
|
|
43
42
|
|
|
44
43
|
let ticketCheckoutAllowed = false;
|
|
44
|
+
let ticketCheckoutDenied = false;
|
|
45
45
|
let paymentEvent: unknown;
|
|
46
46
|
let paymentTickets = new Map<string, WixAvailableTicket>();
|
|
47
47
|
let ticketSelectionVersion = 0;
|
|
48
|
+
let ticketCheckoutInFlight = false;
|
|
49
|
+
let ticketSelectionQueue: Promise<void> = Promise.resolve();
|
|
50
|
+
let ticketSelectionState: S | undefined;
|
|
48
51
|
let lastReady: S | undefined;
|
|
52
|
+
let cancelRsvpInFlight = false;
|
|
49
53
|
const applePurchase = createAttendancePurchase({ kind: "eventsTicket", prefix: "event", pollAction: "himiEventDetail.iapPoll", onChange: () => { void svc.iapPublish(); } });
|
|
50
|
-
|
|
51
|
-
async function priceTicket(event: unknown, id: string, version: number): Promise<boolean> {
|
|
52
|
-
const available = paymentTickets.get(id);
|
|
54
|
+
async function priceTicket(event: unknown, id: string, version: number, ticket = paymentTickets.get(id)): Promise<boolean> {
|
|
53
55
|
if (version !== ticketSelectionVersion) return false;
|
|
54
|
-
await applePurchase.ticket(id, event,
|
|
56
|
+
await applePurchase.ticket(id, event, ticket);
|
|
55
57
|
return version === ticketSelectionVersion;
|
|
56
58
|
}
|
|
57
|
-
|
|
58
|
-
/** A hosted signed-in event-page open is in flight (see openRegistration). */
|
|
59
59
|
let hostedOpenInFlight = false;
|
|
60
|
-
import { stashCopy, copyParam, copyTemplate } from "../_shared/himi-copy.js";
|
|
61
|
-
import { openRichLink } from "../_shared/rich-content-link.js";
|
|
62
|
-
import { richBody } from "../_shared/rich-body.js";
|
|
63
|
-
|
|
64
|
-
interface MapMarker {
|
|
65
|
-
latitude: number;
|
|
66
|
-
longitude: number;
|
|
67
|
-
label?: string;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
interface AttendeeAvatar {
|
|
71
|
-
initials: string;
|
|
72
|
-
}
|
|
73
60
|
|
|
74
61
|
/** One buyer-visible ticket tier. */
|
|
75
62
|
interface TicketTierRow {
|
|
76
63
|
id: string;
|
|
64
|
+
ticketDefinitionId?: string;
|
|
65
|
+
pricingOptionId?: string;
|
|
66
|
+
guestPrice?: string;
|
|
67
|
+
minPrice?: number;
|
|
68
|
+
minimumPriceLabel?: string;
|
|
69
|
+
isGuestPrice?: boolean;
|
|
77
70
|
name: string;
|
|
78
71
|
/** Formatted price, or the free label. */
|
|
79
72
|
priceLine: string;
|
|
@@ -83,6 +76,15 @@ interface TicketTierRow {
|
|
|
83
76
|
/** Selection is rendered by the row itself (bound to `$item.isSelected`). */
|
|
84
77
|
isSelected: boolean;
|
|
85
78
|
notSelected: boolean;
|
|
79
|
+
quantity?: number;
|
|
80
|
+
feeNote?: string;
|
|
81
|
+
description?: string;
|
|
82
|
+
policy?: string;
|
|
83
|
+
priceAmount?: number;
|
|
84
|
+
maxQuantity?: number;
|
|
85
|
+
rowLimit?: number;
|
|
86
|
+
noPolicy?: boolean;
|
|
87
|
+
notAvailable?: boolean;
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
/** Registration CTA kind derived from the event's registration/eventType. */
|
|
@@ -110,7 +112,6 @@ interface S extends AttendancePurchaseFields {
|
|
|
110
112
|
status: "loading" | "ready";
|
|
111
113
|
id: string;
|
|
112
114
|
title: string;
|
|
113
|
-
initial: string;
|
|
114
115
|
when: string;
|
|
115
116
|
location: string;
|
|
116
117
|
/** The plain-text description: RichContent's fallback, the calendar notes, and what shows with no document. */
|
|
@@ -124,17 +125,62 @@ interface S extends AttendancePurchaseFields {
|
|
|
124
125
|
hasImage: boolean;
|
|
125
126
|
noImage: boolean;
|
|
126
127
|
mapUrl: string;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
128
|
+
place: string;
|
|
129
|
+
placeIcon: string;
|
|
130
|
+
noPlace: boolean;
|
|
131
|
+
hidePlaceStatic: boolean;
|
|
132
|
+
price: string;
|
|
133
|
+
zone: string;
|
|
134
|
+
noPrice: boolean;
|
|
135
|
+
hideMapTap: boolean;
|
|
136
|
+
shortDescription: string;
|
|
137
|
+
noShortDescription: boolean;
|
|
138
|
+
noDescription: boolean;
|
|
139
|
+
noDate: boolean;
|
|
140
|
+
dateNotTappable: boolean;
|
|
141
|
+
hideDateStatic: boolean;
|
|
142
|
+
registrationState: string;
|
|
143
|
+
footerLabel: string;
|
|
144
|
+
footerAction: string;
|
|
145
|
+
footerCaption: string;
|
|
146
|
+
hideFooter: boolean;
|
|
147
|
+
noFooterButton: boolean;
|
|
148
|
+
noFooterCaption: boolean;
|
|
149
|
+
notice: string;
|
|
150
|
+
exploreLabel: string;
|
|
151
|
+
noNotice: boolean;
|
|
152
|
+
rsvpLabel: string;
|
|
153
|
+
rsvpId: string;
|
|
154
|
+
rsvpState: "none" | "going" | "waitlist";
|
|
155
|
+
hideRsvpChip: boolean;
|
|
156
|
+
hideGoingChip: boolean;
|
|
157
|
+
hideWaitlistedChip: boolean;
|
|
158
|
+
hideCancelRsvp: boolean;
|
|
159
|
+
hideCancelConfirm: boolean;
|
|
160
|
+
hideRsvpOptions: boolean;
|
|
161
|
+
ticketSheetOpen: boolean;
|
|
162
|
+
rsvpActionsOpen: boolean;
|
|
163
|
+
cancelConfirmOpen: boolean;
|
|
164
|
+
ticketTotal: string;
|
|
165
|
+
ticketCountLabel: string;
|
|
166
|
+
checkoutDisabled: boolean;
|
|
167
|
+
schedule: Array<{ time: string; name: string; detail: string }>;
|
|
168
|
+
fullSchedule: Array<{ time: string; name: string; detail: string }>;
|
|
169
|
+
noSchedule: boolean;
|
|
170
|
+
noFullSchedule: boolean;
|
|
171
|
+
moreDates: Array<{ id: string; date: string }>;
|
|
172
|
+
allDates: Array<{ id: string; date: string }>;
|
|
173
|
+
noMoreDates: boolean;
|
|
174
|
+
noAllDates: boolean;
|
|
175
|
+
policies: Array<{ name: string; text: string }>;
|
|
176
|
+
noPolicies: boolean;
|
|
177
|
+
hideWaitlist: boolean;
|
|
178
|
+
hideScheduled: boolean;
|
|
179
|
+
memberOnly: boolean;
|
|
180
|
+
hideMemberRsvp: boolean;
|
|
181
|
+
hideMemberWaitlist: boolean;
|
|
182
|
+
hideMemberTickets: boolean;
|
|
183
|
+
hideMemberScheduled: boolean;
|
|
138
184
|
/** CTA variant flags — exactly one of {RSVP, tickets, external} shows, or
|
|
139
185
|
* none for NO_REGISTRATION. Unknown/absent registration keeps RSVP (the
|
|
140
186
|
* historical default). */
|
|
@@ -148,6 +194,10 @@ interface S extends AttendancePurchaseFields {
|
|
|
148
194
|
/** Buyer-visible ticket tiers for a ticketed event (empty otherwise). Loaded
|
|
149
195
|
* alongside the event so the price is on screen BEFORE the CTA is tapped. */
|
|
150
196
|
ticketTiers: TicketTierRow[];
|
|
197
|
+
ticketCurrency: string;
|
|
198
|
+
ticketLimitPerOrder: number;
|
|
199
|
+
ticketPurchaseBlocked: boolean;
|
|
200
|
+
regularTicketsHidden: boolean;
|
|
151
201
|
hasTiers: boolean;
|
|
152
202
|
noTiers: boolean;
|
|
153
203
|
/** More than one tier ⇒ the picker is worth showing; exactly one ⇒ the CTA
|
|
@@ -157,8 +207,6 @@ interface S extends AttendancePurchaseFields {
|
|
|
157
207
|
/** The tier the CTA will reserve. Defaults to the cheapest on-sale tier so the
|
|
158
208
|
* button is never dead, and follows the buyer's tap after that. */
|
|
159
209
|
selectedTierId: string;
|
|
160
|
-
ticketPurchaseBlocked: boolean;
|
|
161
|
-
regularTicketsHidden: boolean;
|
|
162
210
|
/** "General · $25" — echoed next to the CTA so the choice is visible. */
|
|
163
211
|
selectedTierLabel: string;
|
|
164
212
|
/** External/ticketed registration URL when the payload carries one. */
|
|
@@ -169,9 +217,9 @@ interface S extends AttendancePurchaseFields {
|
|
|
169
217
|
shareUrl: string;
|
|
170
218
|
calendarStart: number;
|
|
171
219
|
calendarEnd: number;
|
|
172
|
-
|
|
220
|
+
snackbarMessage: string;
|
|
221
|
+
noSnackbar: boolean;
|
|
173
222
|
isAddingCalendar: boolean;
|
|
174
|
-
statusLabel: string;
|
|
175
223
|
emptyText: string;
|
|
176
224
|
errored: boolean;
|
|
177
225
|
notErrored: boolean;
|
|
@@ -199,7 +247,8 @@ function tierPrice(t: WixAvailableTicket): string {
|
|
|
199
247
|
}
|
|
200
248
|
|
|
201
249
|
function tierAvailable(t: WixAvailableTicket): boolean {
|
|
202
|
-
|
|
250
|
+
const salesDetails = (t as WixAvailableTicket & { salesDetails?: { soldOut?: boolean } }).salesDetails;
|
|
251
|
+
return !!t.id && !salesDetails?.soldOut && !/SALE_SCHEDULED|SALE_ENDED|CANCELED|SOLD_OUT/i.test(t.saleStatus ?? "");
|
|
203
252
|
}
|
|
204
253
|
|
|
205
254
|
/** Map the buyer-visible tiers onto rows, cheapest first (so the default pick is
|
|
@@ -207,75 +256,155 @@ function tierAvailable(t: WixAvailableTicket): boolean {
|
|
|
207
256
|
function tierRows(defs: WixAvailableTicket[], selectedId: string): TicketTierRow[] {
|
|
208
257
|
return defs
|
|
209
258
|
.filter((t) => !!t.id)
|
|
259
|
+
.flatMap((t) => {
|
|
260
|
+
const raw = t as WixAvailableTicket & { pricingOptions?: Array<{ id?: string; optionId?: string; name?: string; price?: { amount?: string; value?: string; currency?: string } }>; pricingMethod?: { type?: string; pricingOptions?: { optionDetails?: Array<{ id?: string; optionId?: string; name?: string; price?: { amount?: string; value?: string; currency?: string } }> } } };
|
|
261
|
+
const options = raw.pricingOptions ?? raw.pricingMethod?.pricingOptions?.optionDetails ?? [];
|
|
262
|
+
return options.length ? options.map((option) => ({ ...t, id: `${t.id}:${option.optionId ?? option.id}`, name: [t.name, option.name].filter(Boolean).join(" · "), price: { ...t.price, amount: option.price?.amount ?? option.price?.value ?? t.price?.amount, currency: option.price?.currency ?? t.price?.currency }, __optionId: option.optionId ?? option.id })) : [t as WixAvailableTicket & { __optionId?: string }];
|
|
263
|
+
})
|
|
210
264
|
.slice()
|
|
211
|
-
.sort((a, b) => Number(a.
|
|
265
|
+
.sort((a, b) => Number(a.orderIndex ?? 0) - Number(b.orderIndex ?? 0))
|
|
212
266
|
.map((t) => {
|
|
213
267
|
const available = tierAvailable(t);
|
|
214
268
|
const selected = t.id === selectedId;
|
|
269
|
+
const originalId = t.id?.split(":")[0] ?? "";
|
|
270
|
+
const optionId = (t as WixAvailableTicket & { __optionId?: string }).__optionId;
|
|
271
|
+
const raw = t as WixAvailableTicket & { pricingMethod?: { type?: string; pricingType?: string; guestPrice?: { value?: string } }; pricing?: { pricingType?: string } };
|
|
272
|
+
const isGuestPrice = raw.pricingMethod?.guestPrice != null || /guestPrice/i.test(raw.pricingMethod?.type ?? raw.pricingMethod?.pricingType ?? raw.pricing?.pricingType ?? "");
|
|
273
|
+
const minPrice = Number(raw.pricingMethod?.guestPrice?.value ?? 0);
|
|
215
274
|
return {
|
|
216
275
|
id: t.id ?? "",
|
|
276
|
+
ticketDefinitionId: originalId,
|
|
277
|
+
pricingOptionId: optionId,
|
|
278
|
+
guestPrice: "",
|
|
279
|
+
minPrice,
|
|
217
280
|
name: (t.name || copyParam("event.tierFallbackName", "Ticket")).trim(),
|
|
218
281
|
priceLine: tierPrice(t),
|
|
219
|
-
unavailableNote: available ? "" : copyParam("event.
|
|
282
|
+
unavailableNote: available ? "" : /SALE_SCHEDULED/i.test(t.saleStatus ?? "") ? copyParam("event.onSaleFrom", "On sale {date}").replace("{date}", t.salePeriod?.startDate ? formatWeekdayDateTime(new Date(t.salePeriod.startDate)) : "") : copyParam("event.soldOut", "Sold out"),
|
|
220
283
|
available,
|
|
221
284
|
isSelected: selected,
|
|
222
285
|
notSelected: !selected,
|
|
286
|
+
priceAmount: Number(t.price?.amount ?? 0),
|
|
287
|
+
...(isGuestPrice ? { priceLine: detailCopy("event.payWhatYouWant", "Pay what you want") } : {}),
|
|
288
|
+
...(isGuestPrice ? { priceAmount: minPrice, isGuestPrice: true } : {}),
|
|
289
|
+
...(isGuestPrice ? { minimumPriceLabel: detailCopy("event.minPrice", "Minimum {price}").replace("{price}", formatCurrency(minPrice, t.price?.currency || "USD", "en")) } : {}),
|
|
223
290
|
};
|
|
224
291
|
});
|
|
225
292
|
}
|
|
226
293
|
|
|
227
|
-
|
|
228
|
-
async function loadTiers(eventId: string): Promise<{ rows: TicketTierRow[]; tickets: Map<string, WixAvailableTicket> }> {
|
|
229
|
-
try {
|
|
230
|
-
const response = await queryAvailableTickets(eventId);
|
|
231
|
-
const defs = response.definitions ?? [];
|
|
294
|
+
function loadedTiers(defs: WixAvailableTicket[]): { rows: TicketTierRow[]; tickets: Map<string, WixAvailableTicket> } {
|
|
232
295
|
const tickets = new Map(defs.filter((t) => t.id).map((t) => [t.id!, t]));
|
|
233
296
|
const cheapestAvailable = defs
|
|
234
297
|
.filter(tierAvailable)
|
|
235
298
|
.slice()
|
|
236
299
|
.sort((a, b) => Number(a.price?.amount ?? 0) - Number(b.price?.amount ?? 0))[0];
|
|
237
|
-
|
|
300
|
+
const rows = tierRows(defs, cheapestAvailable?.id ?? "").map((row) => {
|
|
301
|
+
const ticket = tickets.get(row.ticketDefinitionId ?? row.id);
|
|
302
|
+
if (!ticket) return { ...row, quantity: 0, description: "", feeNote: "", policy: "", noPolicy: true, notAvailable: !row.available, notGuestPrice: !row.isGuestPrice, maxQuantity: 0 };
|
|
303
|
+
const policy = (ticket as unknown as { policy?: string; policyText?: string }).policyText || (ticket as unknown as { policy?: string }).policy || "";
|
|
304
|
+
return { ...row, quantity: 0, description: ticket.description || "", feeNote: ((ticket as unknown as { wixFeeConfig?: { type?: string }; feeType?: string }).wixFeeConfig?.type ?? (ticket as unknown as { feeType?: string }).feeType) === "FEE_ADDED_AT_CHECKOUT" ? detailCopy("event.serviceFee", "+ ticket service fee") : "", policy, noPolicy: !policy, notAvailable: !row.available, notGuestPrice: !row.isGuestPrice, priceAmount: row.priceAmount ?? Number(ticket.price?.amount ?? 0), rowLimit: row.available ? ticket.limitPerCheckout ?? 99 : 0, maxQuantity: row.available ? ticket.limitPerCheckout ?? 99 : 0 };
|
|
305
|
+
});
|
|
306
|
+
return { rows, tickets };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Classify the buyer's selected option or bid, rather than the definition's cheapest price. */
|
|
310
|
+
function selectedPaymentTicket(row: TicketTierRow, tickets = paymentTickets): WixAvailableTicket | undefined {
|
|
311
|
+
const ticket = tickets.get(row.ticketDefinitionId ?? row.id);
|
|
312
|
+
if (!ticket) return;
|
|
313
|
+
const priced = tierRows([ticket], row.id).find((next) => next.id === row.id);
|
|
314
|
+
if (!priced) return;
|
|
315
|
+
const amount = priced.isGuestPrice ? Number(row.guestPrice || priced.minPrice || 0) : priced.priceAmount ?? 0;
|
|
316
|
+
return { ...ticket, free: amount === 0, price: { ...ticket.price, amount: String(amount) } };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Best-effort tier read — never sinks the screen if the tier endpoint fails. */
|
|
320
|
+
async function loadTiers(eventId: string): Promise<{ rows: TicketTierRow[]; tickets: Map<string, WixAvailableTicket> }> {
|
|
321
|
+
try {
|
|
322
|
+
return loadedTiers((await queryAvailableTickets(eventId)).definitions ?? []);
|
|
238
323
|
} catch {
|
|
239
324
|
return { rows: [], tickets: new Map() };
|
|
240
325
|
}
|
|
241
326
|
}
|
|
242
327
|
|
|
243
328
|
function derive(s: S): S {
|
|
244
|
-
|
|
329
|
+
// A price refresh can publish a ready snapshot captured before checkout's
|
|
330
|
+
// guard denied the selection. Keep that denial visible until a new selection
|
|
331
|
+
// or load clears it, including across delayed StoreKit publications.
|
|
332
|
+
const snackbarMessage = s.snackbarMessage || (ticketCheckoutDenied ? copyParam("event.purchaseUnavailable", "Purchasing is temporarily unavailable.") : "");
|
|
245
333
|
const hasContent = !!s.title;
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
|
|
334
|
+
const count = s.ticketTiers.reduce((n, row) => n + (row.quantity ?? 0), 0);
|
|
335
|
+
const total = s.ticketTiers.reduce((n, row) => n + (row.isGuestPrice ? Number(row.guestPrice || row.minPrice || 0) : row.priceAmount ?? 0) * (row.quantity ?? 0), 0);
|
|
336
|
+
const ticketTiers = s.ticketTiers.map((row) => {
|
|
337
|
+
const definitionId = row.ticketDefinitionId ?? row.id;
|
|
338
|
+
const sameDefinitionElsewhere = s.ticketTiers.reduce((n, other) => n + ((other.ticketDefinitionId ?? other.id) === definitionId && other.id !== row.id ? other.quantity ?? 0 : 0), 0);
|
|
339
|
+
const selectedElsewhere = s.ticketTiers.reduce((n, other) => n + (other.id === row.id ? 0 : other.quantity ?? 0), 0);
|
|
340
|
+
return { ...row, maxQuantity: Math.max(0, Math.min((row.rowLimit ?? 99) - sameDefinitionElsewhere, s.ticketLimitPerOrder - selectedElsewhere)) };
|
|
341
|
+
});
|
|
342
|
+
const chosenRows = s.ticketTiers.filter((row) => (row.quantity ?? 0) > 0);
|
|
343
|
+
const singleTicketSelected = chosenRows.length === 1 && chosenRows[0]!.quantity === 1;
|
|
344
|
+
const purchase = applePurchase.fields(singleTicketSelected);
|
|
345
|
+
const regularRoute = ticketCheckoutAllowed || applePurchase.usesRegularPlatform();
|
|
250
346
|
const next = {
|
|
251
347
|
...s,
|
|
252
|
-
|
|
348
|
+
ticketTiers,
|
|
253
349
|
...purchase,
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
350
|
+
iapBuyHidden: !singleTicketSelected || purchase.iapBuyHidden,
|
|
351
|
+
// The shared attendance purchase produces a one-shot native poll directive.
|
|
352
|
+
// Let it through once; its commerce adapter consumes it, so later derives
|
|
353
|
+
// cannot replay it from a cached ready state.
|
|
354
|
+
regularTicketsHidden: s.registrationKind !== "tickets" || (singleTicketSelected && !purchase.iapBuyHidden),
|
|
355
|
+
ticketPurchaseBlocked: ticketCheckoutDenied || (s.registrationKind === "tickets" && !regularRoute && purchase.iapBuyHidden),
|
|
258
356
|
hasDescriptionJson: s.descriptionJson !== "",
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
357
|
+
noPlace: !s.place,
|
|
358
|
+
noDate: !s.when,
|
|
359
|
+
dateNotTappable: !s.calendarStart,
|
|
360
|
+
hideDateStatic: !s.when || !!s.calendarStart,
|
|
361
|
+
hidePlaceStatic: !s.place || !!s.mapUrl,
|
|
362
|
+
noPrice: !s.price,
|
|
363
|
+
hideMapTap: !s.place || !s.mapUrl,
|
|
364
|
+
noShortDescription: !s.shortDescription || s.shortDescription.trim() === s.description.trim(),
|
|
365
|
+
noDescription: !s.description && !s.descriptionJson,
|
|
366
|
+
showRsvp: s.registrationState === "rsvp",
|
|
367
|
+
hideRsvp: s.registrationState !== "rsvp" || s.memberOnly,
|
|
266
368
|
hasTiers: s.ticketTiers.length > 0,
|
|
267
369
|
noTiers: s.ticketTiers.length === 0,
|
|
268
370
|
// One tier is not a choice — showing a single-row "picker" would be noise.
|
|
269
371
|
showTierPicker: s.ticketTiers.length > 1,
|
|
270
372
|
hideTierPicker: s.ticketTiers.length <= 1,
|
|
271
373
|
selectedTierLabel: (() => {
|
|
272
|
-
const picked =
|
|
374
|
+
const picked = s.ticketTiers.find((t) => t.id === s.selectedTierId);
|
|
273
375
|
return picked ? [picked.name, picked.priceLine].filter(Boolean).join(" · ") : "";
|
|
274
376
|
})(),
|
|
275
|
-
showTickets: s.
|
|
276
|
-
hideTickets: s.
|
|
277
|
-
showExternal: s.
|
|
278
|
-
hideExternal: s.
|
|
377
|
+
showTickets: s.registrationState === "tickets",
|
|
378
|
+
hideTickets: s.registrationState !== "tickets" || s.memberOnly,
|
|
379
|
+
showExternal: s.registrationState === "external",
|
|
380
|
+
hideExternal: s.registrationState !== "external" || s.memberOnly,
|
|
381
|
+
noNotice: !s.notice,
|
|
382
|
+
snackbarMessage,
|
|
383
|
+
noSnackbar: !snackbarMessage,
|
|
384
|
+
hideWaitlist: s.registrationState !== "waitlist" || s.memberOnly,
|
|
385
|
+
hideScheduled: s.registrationState !== "scheduled",
|
|
386
|
+
memberOnly: s.memberOnly,
|
|
387
|
+
hideMemberRsvp: !s.memberOnly || s.registrationState !== "rsvp",
|
|
388
|
+
hideMemberWaitlist: !s.memberOnly || s.registrationState !== "waitlist",
|
|
389
|
+
hideMemberTickets: !s.memberOnly || s.registrationState !== "tickets",
|
|
390
|
+
// Scheduled registration uses the shared disabled footer, including for members.
|
|
391
|
+
hideMemberScheduled: true,
|
|
392
|
+
hideRsvpChip: s.rsvpState !== "waitlist",
|
|
393
|
+
hideGoingChip: s.rsvpState !== "going",
|
|
394
|
+
hideWaitlistedChip: s.rsvpState !== "waitlist",
|
|
395
|
+
hideCancelRsvp: !s.rsvpId,
|
|
396
|
+
hideCancelConfirm: !s.cancelConfirmOpen,
|
|
397
|
+
hideRsvpOptions: s.cancelConfirmOpen,
|
|
398
|
+
noFooterButton: !s.footerLabel,
|
|
399
|
+
noFooterCaption: !s.footerCaption,
|
|
400
|
+
hideFooter: s.status !== "ready" || s.errored || !s.footerLabel || s.rsvpState !== "none",
|
|
401
|
+
checkoutDisabled: count === 0 || count > s.ticketLimitPerOrder || ticketCheckoutInFlight || !ticketCheckoutAllowed || s.ticketTiers.some((row) => row.isGuestPrice && (row.quantity ?? 0) > 0 && (!String(row.guestPrice ?? "").trim() || Number(row.guestPrice) < (row.minPrice ?? 0) || !Number.isFinite(Number(row.guestPrice)))) || s.ticketTiers.some((row) => (row.quantity ?? 0) > (row.maxQuantity ?? 99)),
|
|
402
|
+
ticketTotal: formatCurrency(total, s.ticketCurrency || "USD", "en"),
|
|
403
|
+
ticketCountLabel: copyParam("event.ticketCount", "{n} tickets, plus fees").replace("{n}", String(count)),
|
|
404
|
+
noSchedule: s.schedule.length === 0,
|
|
405
|
+
noFullSchedule: s.fullSchedule.length === 0,
|
|
406
|
+
noMoreDates: s.moreDates.length === 0,
|
|
407
|
+
noAllDates: s.allDates.length === 0,
|
|
279
408
|
isLoading: s.status === "loading",
|
|
280
409
|
notLoading: s.status !== "loading",
|
|
281
410
|
notErrored: !s.errored,
|
|
@@ -286,7 +415,6 @@ function derive(s: S): S {
|
|
|
286
415
|
? copyParam("event.errorText", "Event could not be loaded.")
|
|
287
416
|
: copyParam("event.notFoundText", "Event not found.")),
|
|
288
417
|
};
|
|
289
|
-
if (next.status === "ready") lastReady = next;
|
|
290
418
|
return next;
|
|
291
419
|
}
|
|
292
420
|
|
|
@@ -320,20 +448,120 @@ function calendarFallbackUrl(s: S, end: number): string {
|
|
|
320
448
|
|
|
321
449
|
/** The Events v3 fieldsets the detail reads (see `WixEventFieldset`). DETAILS carries mainImage and
|
|
322
450
|
* shortDescription: REGISTRATION fills eventType, which skips the query enrichment that used to supply them. */
|
|
323
|
-
export const EVENT_DETAIL_FIELDSETS: WixEventFieldset[] = ["DETAILS", "TEXTS", "REGISTRATION", "URLS", "FORM"];
|
|
451
|
+
export const EVENT_DETAIL_FIELDSETS: WixEventFieldset[] = ["DETAILS", "TEXTS", "REGISTRATION", "URLS", "FORM", "AGENDA"];
|
|
324
452
|
|
|
325
|
-
/** Map
|
|
453
|
+
/** Map registration status and decoded eventType onto the CTA variant. The decoded
|
|
326
454
|
* `eventType` already folds registration.type / registration.initialType /
|
|
327
|
-
* the legacy flat `type` (decodeEvent, #478).
|
|
455
|
+
* the legacy flat `type` (decodeEvent, #478). Without a recognized type or
|
|
456
|
+
* status there is no registration action to offer. */
|
|
328
457
|
export function registrationKindOf(e: WixEvent): RegistrationKind {
|
|
329
|
-
const raw = e as unknown as { registration?: { type?: string; initialType?: string } };
|
|
330
|
-
const value = (
|
|
331
|
-
if (
|
|
332
|
-
|
|
458
|
+
const raw = e as unknown as { registration?: { type?: string; initialType?: string; status?: string } };
|
|
459
|
+
const value = (raw.registration?.type ?? e.eventType ?? raw.registration?.initialType ?? "").toUpperCase();
|
|
460
|
+
if (value === "NONE" || value.includes("NO_REGISTRATION")) return "none";
|
|
461
|
+
// Open statuses select the footer action, so they must also select the
|
|
462
|
+
// inventory read and action guards when type metadata is absent or stale.
|
|
463
|
+
switch (raw.registration?.status?.toUpperCase()) {
|
|
464
|
+
case "OPEN_TICKETS": return "tickets";
|
|
465
|
+
case "OPEN_EXTERNAL": return "external";
|
|
466
|
+
case "OPEN_RSVP":
|
|
467
|
+
case "OPEN_RSVP_WAITLIST_ONLY":
|
|
468
|
+
case "SCHEDULED_RSVP": return "rsvp";
|
|
469
|
+
}
|
|
333
470
|
if (value.includes("EXTERNAL")) return "external";
|
|
334
471
|
if (value.includes("TICKET")) return "tickets";
|
|
335
472
|
if (value.includes("RSVP")) return "rsvp";
|
|
336
|
-
return "
|
|
473
|
+
return "none";
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function detailCopy(key: string, fallback: string): string { return copyParam(key, fallback); }
|
|
477
|
+
|
|
478
|
+
function detailDate(e: WixEvent): { line: string; zone: string; start: number; end: number } {
|
|
479
|
+
const raw = e as unknown as {
|
|
480
|
+
dateAndTimeSettings?: { startDate?: string; endDate?: string; dateAndTimeTbd?: boolean; dateAndTimeTbdMessage?: string; timeZoneId?: string; showTimeZone?: boolean; hideEndDate?: boolean };
|
|
481
|
+
scheduling?: { startDate?: string; endDate?: string; timeZoneId?: string; tbd?: string; endDateHidden?: boolean; config?: { startDate?: string; endDate?: string; timeZoneId?: string } };
|
|
482
|
+
};
|
|
483
|
+
const settings = raw.dateAndTimeSettings ?? {};
|
|
484
|
+
const schedule = raw.scheduling;
|
|
485
|
+
const startValue = settings.startDate ?? schedule?.config?.startDate ?? schedule?.startDate;
|
|
486
|
+
const endValue = settings.endDate ?? schedule?.config?.endDate ?? schedule?.endDate;
|
|
487
|
+
const timeZoneId = settings.timeZoneId ?? schedule?.config?.timeZoneId ?? schedule?.timeZoneId;
|
|
488
|
+
const start = startValue ? new Date(startValue) : null;
|
|
489
|
+
const end = endValue ? new Date(endValue) : null;
|
|
490
|
+
const validStart = start && !Number.isNaN(start.getTime());
|
|
491
|
+
if (settings.dateAndTimeTbd || (!validStart && schedule?.tbd)) return { line: settings.dateAndTimeTbdMessage || schedule?.tbd || detailCopy("event.dateTbd", "Date to be announced"), zone: "", start: 0, end: 0 };
|
|
492
|
+
if (!validStart) return { line: "", zone: "", start: 0, end: 0 };
|
|
493
|
+
const zone = timeZoneId;
|
|
494
|
+
const format = (d: Date): string => formatWeekdayDateTime(d, "en-US", zone);
|
|
495
|
+
const a = format(start);
|
|
496
|
+
const b = end && !Number.isNaN(end.getTime()) ? format(end) : "";
|
|
497
|
+
const parts = (date: Date) => {
|
|
498
|
+
const time = formatClock(date, "en-US", zone);
|
|
499
|
+
const clock = time.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)?$/i);
|
|
500
|
+
return { day: formatWeekdayMonthDay(date, "en-US", zone), hour: clock?.[1] ?? "", minute: clock?.[2] ?? "", period: clock?.[3] ?? "", time, calendarDay: calendarDay(date, zone) };
|
|
501
|
+
};
|
|
502
|
+
const startParts = parts(start);
|
|
503
|
+
const endParts = end && !Number.isNaN(end.getTime()) ? parts(end) : undefined;
|
|
504
|
+
const startDisplayDay = startParts.day.replace(/, \d{4}$/, "");
|
|
505
|
+
const hideEndDate = settings.hideEndDate ?? schedule?.endDateHidden;
|
|
506
|
+
const line = hideEndDate || !endParts ? `${startDisplayDay} at ${startParts.time}` : startParts.calendarDay === endParts.calendarDay
|
|
507
|
+
? `${startDisplayDay} · ${startParts.hour}:${startParts.minute}${startParts.period !== endParts.period ? ` ${startParts.period}` : ""} – ${endParts.time}`
|
|
508
|
+
: `${a} – ${b}`;
|
|
509
|
+
let zoneLine = "";
|
|
510
|
+
if (settings.showTimeZone && zone) zoneLine = formatTimeZoneLabel(start, zone, "en");
|
|
511
|
+
return { line, zone: zoneLine, start: start.getTime(), end: end?.getTime() ?? start.getTime() + 3600000 };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
type EventDetailMoney = { formattedValue?: string; value?: string; currency?: string };
|
|
515
|
+
|
|
516
|
+
function detailMoney(money?: EventDetailMoney): string {
|
|
517
|
+
if (money?.formattedValue) return money.formattedValue;
|
|
518
|
+
const value = money?.value;
|
|
519
|
+
if (value == null || String(value).trim() === "" || !money?.currency || !Number.isFinite(Number(value))) return "";
|
|
520
|
+
return formatCurrency(Number(value), money.currency, "en");
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function detailPlace(e: WixEvent): { value: string; mapUrl: string; hideMapTap: boolean; icon: string } {
|
|
524
|
+
const raw = e as unknown as { location?: { type?: string; name?: string; locationTbd?: boolean; address?: { formattedAddress?: string; formatted?: string } | string } };
|
|
525
|
+
const place = raw.location;
|
|
526
|
+
const type = (place?.type ?? "").toUpperCase();
|
|
527
|
+
if (place?.locationTbd) return { value: place.name || detailCopy("event.locationTbd", "Location to be announced"), mapUrl: "", hideMapTap: true, icon: "location" };
|
|
528
|
+
if (type === "ONLINE") return { value: detailCopy("event.onlineEvent", "Online event"), mapUrl: "", hideMapTap: true, icon: "globe" };
|
|
529
|
+
const address = eventAddress({ ...e, location: { ...e.location, name: "" } } as WixEvent);
|
|
530
|
+
const value = [place?.name, address].filter(Boolean).join(", ") || place?.name || "";
|
|
531
|
+
const geo = (e.location as unknown as { coordinates?: { latitude?: number; longitude?: number; lat?: number; lng?: number }; location?: { latitude?: number; longitude?: number; lat?: number; lng?: number } })?.coordinates
|
|
532
|
+
?? (e.location as unknown as { location?: { latitude?: number; longitude?: number; lat?: number; lng?: number } })?.location;
|
|
533
|
+
const hasCoordinates = typeof (geo?.latitude ?? geo?.lat) === "number" && typeof (geo?.longitude ?? geo?.lng) === "number";
|
|
534
|
+
const mapped = address || hasCoordinates ? eventMapUrl(e) : "";
|
|
535
|
+
return { value, mapUrl: mapped, hideMapTap: !mapped, icon: "location" };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function eventStatus(e: WixEvent): string {
|
|
539
|
+
const raw = e as unknown as { status?: string };
|
|
540
|
+
return (raw.status ?? e.eventStatus ?? "").toUpperCase();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function registrationUi(e: WixEvent, now: number): { state: string; label: string; action: string; caption: string; notice: string } {
|
|
544
|
+
const raw = e as unknown as { registration?: { status?: string; registrationPaused?: boolean; registrationDisabled?: boolean; allowedGuestTypes?: string; rsvp?: { startDate?: string }; tickets?: { soldOut?: boolean }; external?: { url?: string } }; form?: { messages?: { registrationClosed?: { message?: string; exploreEventsActionLabel?: string }; ticketsUnavailable?: { message?: string; exploreEventsActionLabel?: string } } } };
|
|
545
|
+
const r = raw.registration ?? {};
|
|
546
|
+
const status = eventStatus(e);
|
|
547
|
+
const type = registrationKindOf(e);
|
|
548
|
+
if (status === "CANCELED") return { state: "notice", label: "", action: "", caption: "", notice: detailCopy("event.canceledNotice", "This event was canceled.") };
|
|
549
|
+
if (status === "ENDED") return { state: "notice", label: "", action: "", caption: "", notice: detailCopy("event.endedNotice", "This event has ended.") };
|
|
550
|
+
if (type === "none" || r.registrationDisabled) return { state: "none", label: "", action: "", caption: "", notice: "" };
|
|
551
|
+
const ownerClosed = raw.form?.messages?.registrationClosed?.message;
|
|
552
|
+
const ownerSold = raw.form?.messages?.ticketsUnavailable?.message;
|
|
553
|
+
if (["CLOSED_AUTOMATICALLY", "CLOSED_MANUALLY"].includes(r.status || "") || r.registrationPaused) return { state: "notice", label: "", action: "", caption: "", notice: ownerClosed || detailCopy("event.closedNotice", "Registration is closed.") };
|
|
554
|
+
if (type === "tickets" && r.tickets?.soldOut) return { state: "notice", label: "", action: "", caption: "", notice: ownerSold || detailCopy("event.soldOutNotice", "Tickets are sold out.") };
|
|
555
|
+
if (r.status === "SCHEDULED_RSVP") return { state: "scheduled", label: detailCopy("event.rsvpCta", "RSVP"), action: "", caption: detailCopy("event.opensCaption", "RSVP opens {date}.").replace("{date}", r.rsvp?.startDate ? formatWeekdayDateTime(new Date(r.rsvp.startDate)) : ""), notice: "" };
|
|
556
|
+
if (r.status === "OPEN_RSVP_WAITLIST_ONLY") return { state: "waitlist", label: detailCopy("event.waitlistCta", "Join Waitlist"), action: "himi.open", caption: detailCopy("event.fullCaption", "This event is full."), notice: "" };
|
|
557
|
+
if (r.status === "OPEN_TICKETS" || type === "tickets") return { state: "tickets", label: detailCopy("event.ticketsCta", "Buy Tickets"), action: "openTickets", caption: "", notice: "" };
|
|
558
|
+
if (r.status === "OPEN_EXTERNAL" || type === "external") return registrationUrlOf(e)
|
|
559
|
+
? { state: "external", label: detailCopy("event.externalCta", "Register"), action: "openRegistration", caption: "", notice: "" }
|
|
560
|
+
: { state: "none", label: "", action: "", caption: "", notice: "" };
|
|
561
|
+
if (type === "rsvp" && r.status === "OPEN_RSVP") return { state: "rsvp", label: detailCopy("event.rsvpCta", "RSVP"), action: "himi.open", caption: "", notice: "" };
|
|
562
|
+
if (r.status === "OPEN_RSVP" || (type === "rsvp" && !r.status)) return { state: "rsvp", label: detailCopy("event.rsvpCta", "RSVP"), action: "himi.open", caption: "", notice: "" };
|
|
563
|
+
void now;
|
|
564
|
+
return { state: "none", label: "", action: "", caption: "", notice: "" };
|
|
337
565
|
}
|
|
338
566
|
|
|
339
567
|
/** External registration URL / event page URL from the raw payload ("" when absent). */
|
|
@@ -365,48 +593,6 @@ export function eventPageUrlOf(e: WixEvent): string {
|
|
|
365
593
|
return "";
|
|
366
594
|
}
|
|
367
595
|
|
|
368
|
-
/** Geo coordinates → a 0/1-entry marker set for the inline schematic map. */
|
|
369
|
-
export function eventMapMarkers(e: WixEvent): MapMarker[] {
|
|
370
|
-
const loc = e.location as unknown as {
|
|
371
|
-
name?: string;
|
|
372
|
-
location?: { latitude?: number; longitude?: number; lat?: number; lng?: number };
|
|
373
|
-
coordinates?: { latitude?: number; longitude?: number; lat?: number; lng?: number };
|
|
374
|
-
} | undefined;
|
|
375
|
-
const geo = loc?.coordinates ?? loc?.location;
|
|
376
|
-
const latitude = geo?.latitude ?? geo?.lat;
|
|
377
|
-
const longitude = geo?.longitude ?? geo?.lng;
|
|
378
|
-
if (typeof latitude !== "number" || typeof longitude !== "number") return [];
|
|
379
|
-
return [{ latitude, longitude, ...(loc?.name ? { label: loc.name } : {}) }];
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/** Best-effort attendee summary: "N going" + up-to-6 initials for the avatar
|
|
383
|
-
* strip. The guest-list endpoint is owner-scoped — a visitor rung throws and
|
|
384
|
-
* the caller collapses the strip (never fails the screen). */
|
|
385
|
-
async function attendeeSummary(eventId: string): Promise<{ goingLabel: string; avatars: AttendeeAvatar[] }> {
|
|
386
|
-
const nameOf = (g: WixGuestRsvp | WixGuestTicket): string =>
|
|
387
|
-
[g.firstName, g.lastName].filter(Boolean).join(" ").trim();
|
|
388
|
-
try {
|
|
389
|
-
let list = await getEventGuestList(eventId, { ticketed: false });
|
|
390
|
-
if (!(list.rsvps?.length || list.total)) {
|
|
391
|
-
list = await getEventGuestList(eventId, { ticketed: true });
|
|
392
|
-
}
|
|
393
|
-
const guests: Array<WixGuestRsvp | WixGuestTicket> = list.rsvps?.length ? list.rsvps : (list.tickets ?? []);
|
|
394
|
-
const going = guests
|
|
395
|
-
.filter((g) => {
|
|
396
|
-
const status = (g.status ?? "").toUpperCase();
|
|
397
|
-
return !status || ["YES", "ARRIVED", "PAID", "FREE", "OFFLINE_PENDING", "WAITING"].includes(status);
|
|
398
|
-
});
|
|
399
|
-
const total = list.facets?.status?.total ?? list.total ?? going.length;
|
|
400
|
-
if (!total) return { goingLabel: "", avatars: [] };
|
|
401
|
-
return {
|
|
402
|
-
goingLabel: copyTemplate("event.goingLabel", "{count} going", { count: total }),
|
|
403
|
-
avatars: going.slice(0, 6).map((g) => ({ initials: initials(nameOf(g) || g.email || "Guest") })),
|
|
404
|
-
};
|
|
405
|
-
} catch {
|
|
406
|
-
return { goingLabel: "", avatars: [] };
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
596
|
/** The description pair: the v3 Ricos `description` for RichContent, and its plain text. A string description
|
|
411
597
|
* (or none) keeps `eventDescription`'s legacy read, with no document. */
|
|
412
598
|
function eventBody(e: WixEvent): { description: string; descriptionJson: string } {
|
|
@@ -415,33 +601,41 @@ function eventBody(e: WixEvent): { description: string; descriptionJson: string
|
|
|
415
601
|
return { description: bodyText, descriptionJson: bodyJson };
|
|
416
602
|
}
|
|
417
603
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
604
|
+
type WixCall = (method: string, path: string, body?: unknown) => Promise<unknown>;
|
|
605
|
+
function wixCall(): WixCall | undefined { return (globalThis as { himalaya?: { wixApi?: { call?: WixCall } } }).himalaya?.wixApi?.call; }
|
|
606
|
+
type OptionalRead = { ok: true; value: Record<string, unknown> } | { ok: false; code: "unavailable" };
|
|
607
|
+
async function bestEffort(method: string, path: string, body?: unknown): Promise<OptionalRead> {
|
|
422
608
|
try {
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
if (!match) return e;
|
|
426
|
-
// [rico] #3372 — the query's rows are read without TEXTS, so they carry no Ricos `description`: keep the
|
|
427
|
-
// one this detail fetched, or the enrichment would silently drop the document on every real site.
|
|
428
|
-
const description = (e as unknown as { description?: unknown }).description;
|
|
429
|
-
return description === undefined ? match : ({ ...match, description } as unknown as WixEvent);
|
|
609
|
+
const call = wixCall();
|
|
610
|
+
return call ? { ok: true, value: await call(method, path, body) as Record<string, unknown> } : { ok: false, code: "unavailable" };
|
|
430
611
|
} catch {
|
|
431
|
-
return
|
|
612
|
+
return { ok: false, code: "unavailable" };
|
|
432
613
|
}
|
|
433
614
|
}
|
|
615
|
+
async function signedInMember(): Promise<boolean> {
|
|
616
|
+
try { const identity = await dispatch("himi.wix.memberIdentity") as { email?: string } | null; return !!identity?.email; } catch { return false; }
|
|
617
|
+
}
|
|
434
618
|
|
|
435
|
-
const defineService = withOnlineWrites(registerService, ["buyApple", "openRegistration"], {
|
|
619
|
+
const defineService = withOnlineWrites(registerService, ["buyApple", "openRegistration", "checkout", "cancelRsvp"], {
|
|
436
620
|
derive,
|
|
437
|
-
messageFields: ["
|
|
621
|
+
messageFields: ["snackbarMessage"],
|
|
438
622
|
requiresConnectivity: (action, state) => action !== "openRegistration"
|
|
439
623
|
|| (state.registrationKind === "tickets" && !!state.id && !isHostedWorker()),
|
|
440
624
|
});
|
|
441
|
-
|
|
625
|
+
type DetailActions = {
|
|
442
626
|
load: (s: S, arg?: unknown) => Promise<S | undefined>;
|
|
443
627
|
openMap: (s: S) => Promise<S>;
|
|
444
|
-
|
|
628
|
+
openTickets: (s: S) => Promise<S | undefined>;
|
|
629
|
+
closeTickets: (s: S) => S;
|
|
630
|
+
setQuantity: (s: S, arg?: unknown) => Promise<S | undefined>;
|
|
631
|
+
setGuestPrice: (s: S, arg?: unknown) => S;
|
|
632
|
+
checkout: (s: S) => Promise<S | undefined>;
|
|
633
|
+
openRsvpActions: (s: S) => S;
|
|
634
|
+
closeRsvpActions: (s: S) => S;
|
|
635
|
+
beginCancelRsvp: (s: S) => S;
|
|
636
|
+
keepRsvp: (s: S) => S;
|
|
637
|
+
cancelRsvp: (s: S) => Promise<S>;
|
|
638
|
+
noop: (s: S) => S;
|
|
445
639
|
openRegistration: (s: S) => Promise<S>;
|
|
446
640
|
share: (s: S) => Promise<S>;
|
|
447
641
|
addToCalendar: (s: S) => Promise<S>;
|
|
@@ -452,16 +646,65 @@ const svc = defineService<S, {
|
|
|
452
646
|
iapPublish: (s: S) => S;
|
|
453
647
|
iapRefresh: (s: S) => Promise<S>;
|
|
454
648
|
iapPoll: (s: S) => Promise<S>;
|
|
455
|
-
}
|
|
456
|
-
|
|
649
|
+
};
|
|
650
|
+
const svc: ReturnType<typeof defineService<S, DetailActions>> = defineService<S, DetailActions>("himiEventDetail", {
|
|
651
|
+
notHydrated: [...ATTENDANCE_NOT_HYDRATED, "ticketPurchaseBlocked", "ticketSheetOpen", "rsvpActionsOpen", "cancelConfirmOpen", "rsvpId", "rsvpState", "rsvpLabel"],
|
|
457
652
|
state: derive({
|
|
458
653
|
...ATTENDANCE_INITIAL,
|
|
459
654
|
status: "loading",
|
|
460
655
|
id: inputId(),
|
|
461
656
|
title: "",
|
|
462
|
-
initial: "E",
|
|
463
657
|
when: "",
|
|
464
658
|
location: "",
|
|
659
|
+
place: "",
|
|
660
|
+
placeIcon: "location",
|
|
661
|
+
noPlace: true,
|
|
662
|
+
hidePlaceStatic: true,
|
|
663
|
+
price: "",
|
|
664
|
+
zone: "",
|
|
665
|
+
noPrice: true,
|
|
666
|
+
hideMapTap: true,
|
|
667
|
+
shortDescription: "",
|
|
668
|
+
noShortDescription: true,
|
|
669
|
+
noDescription: true,
|
|
670
|
+
noDate: true,
|
|
671
|
+
dateNotTappable: true,
|
|
672
|
+
hideDateStatic: true,
|
|
673
|
+
registrationState: "none",
|
|
674
|
+
footerLabel: "",
|
|
675
|
+
footerAction: "",
|
|
676
|
+
footerCaption: "",
|
|
677
|
+
hideFooter: true,
|
|
678
|
+
noFooterButton: true,
|
|
679
|
+
noFooterCaption: true,
|
|
680
|
+
notice: "",
|
|
681
|
+
exploreLabel: detailCopy("event.exploreCta", "Explore Events"),
|
|
682
|
+
noNotice: true,
|
|
683
|
+
rsvpLabel: "",
|
|
684
|
+
rsvpId: "",
|
|
685
|
+
rsvpState: "none",
|
|
686
|
+
hideRsvpChip: true,
|
|
687
|
+
hideGoingChip: true,
|
|
688
|
+
hideWaitlistedChip: true,
|
|
689
|
+
hideCancelRsvp: true,
|
|
690
|
+
hideCancelConfirm: true,
|
|
691
|
+
hideRsvpOptions: false,
|
|
692
|
+
ticketSheetOpen: false,
|
|
693
|
+
rsvpActionsOpen: false,
|
|
694
|
+
cancelConfirmOpen: false,
|
|
695
|
+
ticketTotal: formatCurrency(0, "USD", "en"),
|
|
696
|
+
ticketCountLabel: copyParam("event.ticketCount", "{n} tickets, plus fees").replace("{n}", "0"),
|
|
697
|
+
checkoutDisabled: true,
|
|
698
|
+
schedule: [],
|
|
699
|
+
fullSchedule: [],
|
|
700
|
+
noSchedule: true,
|
|
701
|
+
noFullSchedule: true,
|
|
702
|
+
moreDates: [],
|
|
703
|
+
allDates: [],
|
|
704
|
+
noMoreDates: true,
|
|
705
|
+
noAllDates: true,
|
|
706
|
+
policies: [],
|
|
707
|
+
noPolicies: true,
|
|
465
708
|
description: "",
|
|
466
709
|
descriptionJson: "",
|
|
467
710
|
hasDescriptionJson: false,
|
|
@@ -469,24 +712,25 @@ const svc = defineService<S, {
|
|
|
469
712
|
hasImage: false,
|
|
470
713
|
noImage: true,
|
|
471
714
|
mapUrl: "",
|
|
472
|
-
noMap: true,
|
|
473
|
-
mapMarkers: [],
|
|
474
|
-
hasMapMarkers: false,
|
|
475
|
-
noMapMarkers: true,
|
|
476
|
-
goingLabel: "",
|
|
477
|
-
attendeeAvatars: [],
|
|
478
|
-
hasAttendees: false,
|
|
479
|
-
noAttendees: true,
|
|
480
715
|
registrationKind: "rsvp",
|
|
481
716
|
ticketPurchaseBlocked: true,
|
|
482
|
-
regularTicketsHidden: true,
|
|
483
717
|
showRsvp: true,
|
|
484
718
|
hideRsvp: false,
|
|
485
719
|
showTickets: false,
|
|
486
720
|
hideTickets: true,
|
|
487
721
|
showExternal: false,
|
|
488
722
|
hideExternal: true,
|
|
723
|
+
hideScheduled: true,
|
|
724
|
+
hideWaitlist: true,
|
|
725
|
+
memberOnly: false,
|
|
726
|
+
hideMemberRsvp: true,
|
|
727
|
+
hideMemberWaitlist: true,
|
|
728
|
+
hideMemberTickets: true,
|
|
729
|
+
hideMemberScheduled: true,
|
|
489
730
|
ticketTiers: [],
|
|
731
|
+
ticketCurrency: "USD",
|
|
732
|
+
ticketLimitPerOrder: 99,
|
|
733
|
+
regularTicketsHidden: true,
|
|
490
734
|
hasTiers: false,
|
|
491
735
|
noTiers: true,
|
|
492
736
|
showTierPicker: false,
|
|
@@ -498,9 +742,9 @@ const svc = defineService<S, {
|
|
|
498
742
|
shareUrl: "",
|
|
499
743
|
calendarStart: 0,
|
|
500
744
|
calendarEnd: 0,
|
|
501
|
-
|
|
745
|
+
snackbarMessage: "",
|
|
746
|
+
noSnackbar: true,
|
|
502
747
|
isAddingCalendar: false,
|
|
503
|
-
statusLabel: "",
|
|
504
748
|
emptyText: "",
|
|
505
749
|
errored: false,
|
|
506
750
|
notErrored: true,
|
|
@@ -509,18 +753,285 @@ const svc = defineService<S, {
|
|
|
509
753
|
noEmpty: true,
|
|
510
754
|
}),
|
|
511
755
|
actions: {
|
|
512
|
-
|
|
756
|
+
openTickets: async (s) => {
|
|
757
|
+
if (s.ticketTiers.length) return derive({ ...s, ticketSheetOpen: true });
|
|
758
|
+
const version = ticketSelectionVersion;
|
|
759
|
+
const loaded = await loadTiers(s.id);
|
|
760
|
+
const current = svc.getState();
|
|
761
|
+
if (version !== ticketSelectionVersion || current.id !== s.id) return;
|
|
762
|
+
if (!loaded.rows.length) return derive({ ...current, ticketSheetOpen: false, snackbarMessage: detailCopy("event.purchaseUnavailable", "Purchasing is temporarily unavailable.") });
|
|
763
|
+
paymentTickets = loaded.tickets;
|
|
764
|
+
ticketSelectionState = derive({ ...current, ticketTiers: loaded.rows.map((row) => ({ ...row, rowLimit: Math.min(row.rowLimit ?? 99, s.ticketLimitPerOrder) })), ticketCurrency: loaded.tickets.values().next().value?.price?.currency || s.ticketCurrency, ticketSheetOpen: true, snackbarMessage: "" });
|
|
765
|
+
lastReady = ticketSelectionState;
|
|
766
|
+
return ticketSelectionState;
|
|
767
|
+
},
|
|
768
|
+
closeTickets: (s) => derive({ ...s, ticketSheetOpen: false }),
|
|
769
|
+
openRsvpActions: (s) => derive({ ...s, rsvpActionsOpen: true, cancelConfirmOpen: false, snackbarMessage: "" }),
|
|
770
|
+
closeRsvpActions: (s) => derive({ ...s, rsvpActionsOpen: false, cancelConfirmOpen: false }),
|
|
771
|
+
beginCancelRsvp: (s) => derive({ ...s, cancelConfirmOpen: !!s.rsvpId, snackbarMessage: "" }),
|
|
772
|
+
keepRsvp: (s) => derive({ ...s, cancelConfirmOpen: false }),
|
|
773
|
+
cancelRsvp: async (s): Promise<S> => {
|
|
774
|
+
if (!s.rsvpId || !s.id || cancelRsvpInFlight) return derive({ ...s, rsvpActionsOpen: false, cancelConfirmOpen: false });
|
|
775
|
+
cancelRsvpInFlight = true;
|
|
776
|
+
try {
|
|
777
|
+
const call = wixCall();
|
|
778
|
+
if (!call) throw new Error("Wix API is unavailable");
|
|
779
|
+
await call("DELETE", `/events/v2/rsvps/${encodeURIComponent(s.rsvpId)}`);
|
|
780
|
+
forgetRsvp(s.id);
|
|
781
|
+
return derive({
|
|
782
|
+
...s,
|
|
783
|
+
rsvpId: "",
|
|
784
|
+
rsvpState: "none",
|
|
785
|
+
rsvpLabel: "",
|
|
786
|
+
rsvpActionsOpen: false,
|
|
787
|
+
cancelConfirmOpen: false,
|
|
788
|
+
snackbarMessage: copyParam("event.rsvpCanceled", "Your RSVP is canceled."),
|
|
789
|
+
});
|
|
790
|
+
} catch {
|
|
791
|
+
return derive({
|
|
792
|
+
...s,
|
|
793
|
+
rsvpActionsOpen: false,
|
|
794
|
+
cancelConfirmOpen: false,
|
|
795
|
+
snackbarMessage: copyParam("event.cancelFailed", "Couldn't cancel your RSVP. Try again."),
|
|
796
|
+
});
|
|
797
|
+
} finally {
|
|
798
|
+
cancelRsvpInFlight = false;
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
noop: (s) => derive(s),
|
|
802
|
+
setQuantity: async (inputState, arg): Promise<S | undefined> => {
|
|
803
|
+
if (ticketCheckoutInFlight) return derive(inputState);
|
|
804
|
+
const input = arg as { id?: string; value?: number; quantity?: number } | undefined;
|
|
805
|
+
const id = typeof input === "string" ? input : input?.id;
|
|
806
|
+
const next = Number(input?.value ?? input?.quantity ?? 0);
|
|
807
|
+
if (!id || !Number.isFinite(next)) return derive(inputState);
|
|
808
|
+
// Invalidate the previous Apple form synchronously with the row action.
|
|
809
|
+
// A purchase tapped before the async route/price checks finish must not
|
|
810
|
+
// submit details belonging to the ticket that was selected previously.
|
|
811
|
+
const version = ++ticketSelectionVersion;
|
|
812
|
+
ticketCheckoutDenied = false;
|
|
813
|
+
applePurchase.invalidate();
|
|
814
|
+
ticketSelectionQueue = ticketSelectionQueue.catch(() => {}).then(async () => {
|
|
815
|
+
if (ticketCheckoutInFlight) return;
|
|
816
|
+
// Worker actions can overlap while capability checks await. Start each
|
|
817
|
+
// update from the last selection so edits to independent rows compose.
|
|
818
|
+
const serviceState = svc.getState();
|
|
819
|
+
const base = serviceState.status === "ready" && serviceState.id === inputState.id ? { ...inputState, ...serviceState } : inputState;
|
|
820
|
+
const s = ticketSelectionState?.status === "ready" && ticketSelectionState.id === base.id
|
|
821
|
+
? { ...base, selectedTierId: ticketSelectionState.selectedTierId, ticketTiers: ticketSelectionState.ticketTiers }
|
|
822
|
+
: base;
|
|
823
|
+
const ticketTiers = s.ticketTiers.map((row) => row.id !== id || !row.available ? row : { ...row, quantity: Math.max(0, Math.min(row.maxQuantity ?? 99, Math.floor(next))) });
|
|
824
|
+
const chosen = ticketTiers.filter((row) => (row.quantity ?? 0) > 0);
|
|
825
|
+
let selectedTierId = s.selectedTierId;
|
|
826
|
+
if (chosen.length === 1) selectedTierId = chosen[0]!.id;
|
|
827
|
+
else if (chosen.length === 0 && id === selectedTierId) selectedTierId = ticketTiers.find((row) => row.available)?.id ?? "";
|
|
828
|
+
const selected = ticketTiers.find((row) => row.id === selectedTierId);
|
|
829
|
+
let regularAllowed: boolean;
|
|
830
|
+
if (chosen.length <= 1 && (chosen.length === 0 || chosen[0]!.quantity === 1)) {
|
|
831
|
+
regularAllowed = await mayUseRegularTicketCheckout(paymentEvent, selected ? selectedPaymentTicket(selected) : undefined);
|
|
832
|
+
} else {
|
|
833
|
+
regularAllowed = (await Promise.all(chosen.map((row) => mayUseRegularTicketCheckout(paymentEvent, selectedPaymentTicket(row))))).every(Boolean);
|
|
834
|
+
}
|
|
835
|
+
if (version === ticketSelectionVersion) ticketCheckoutAllowed = regularAllowed;
|
|
836
|
+
const selectionState = derive({ ...s, selectedTierId, ticketTiers: ticketTiers.map((row) => ({ ...row, isSelected: row.id === selectedTierId, notSelected: row.id !== selectedTierId })) });
|
|
837
|
+
ticketSelectionState = selectionState;
|
|
838
|
+
if (selectionState.status === "ready") lastReady = selectionState;
|
|
839
|
+
if (version === ticketSelectionVersion && selectedTierId && chosen.length === 1 && !regularAllowed) {
|
|
840
|
+
const ticketId = selected?.ticketDefinitionId ?? selectedTierId;
|
|
841
|
+
if (!await priceTicket(paymentEvent, ticketId, version, selected ? selectedPaymentTicket(selected) : undefined)) return;
|
|
842
|
+
}
|
|
843
|
+
// Keep edits and sheet state made while route and price checks awaited.
|
|
844
|
+
const currentServiceState = svc.getState();
|
|
845
|
+
const latest = currentServiceState.status === "ready" && currentServiceState.id === s.id ? { ...s, ...currentServiceState } : s;
|
|
846
|
+
const quantities = new Map(ticketTiers.map((row) => [row.id, row.quantity ?? 0]));
|
|
847
|
+
const latestRows = latest.ticketTiers.map((row) => ({ ...row, quantity: quantities.get(row.id) ?? row.quantity ?? 0, isSelected: row.id === selectedTierId, notSelected: row.id !== selectedTierId }));
|
|
848
|
+
ticketSelectionState = derive({ ...latest, selectedTierId, ticketTiers: latestRows });
|
|
849
|
+
if (ticketSelectionState.status === "ready") lastReady = ticketSelectionState;
|
|
850
|
+
});
|
|
851
|
+
await ticketSelectionQueue;
|
|
852
|
+
return ticketSelectionState;
|
|
853
|
+
},
|
|
854
|
+
setGuestPrice: (s, arg) => {
|
|
855
|
+
if (ticketCheckoutInFlight) return derive(s);
|
|
856
|
+
const input = arg as { id?: string; value?: string; text?: string } | undefined;
|
|
857
|
+
const id = input?.id;
|
|
858
|
+
const value = String(input?.value ?? input?.text ?? "").replace(/[^\d.]/g, "");
|
|
859
|
+
if (!id || !/^\d*(?:\.\d{0,2})?$/.test(value)) return derive(s);
|
|
860
|
+
ticketSelectionState = derive({ ...s, ticketTiers: s.ticketTiers.map((row) => row.id === id ? { ...row, guestPrice: value, priceAmount: Number(value || row.minPrice || 0) } : row) });
|
|
861
|
+
if (ticketSelectionState.status === "ready") lastReady = ticketSelectionState;
|
|
862
|
+
return ticketSelectionState;
|
|
863
|
+
},
|
|
864
|
+
checkout: async (initial) => {
|
|
865
|
+
if (ticketCheckoutInFlight) return derive(initial);
|
|
866
|
+
// A Stepper action can still be awaiting its IAP/price route check when
|
|
867
|
+
// Checkout is tapped. Wait until the selection queue stops growing, then
|
|
868
|
+
// take the service's latest state before validating or reserving.
|
|
869
|
+
while (true) {
|
|
870
|
+
const pendingSelection = ticketSelectionQueue;
|
|
871
|
+
await pendingSelection;
|
|
872
|
+
if (pendingSelection === ticketSelectionQueue) break;
|
|
873
|
+
}
|
|
874
|
+
if (ticketCheckoutInFlight) return derive(svc.getState());
|
|
875
|
+
const current = svc.getState();
|
|
876
|
+
let s = ticketSelectionState?.status === "ready" && ticketSelectionState.id === initial.id
|
|
877
|
+
? ticketSelectionState
|
|
878
|
+
: current.status === "ready" && current.id === initial.id ? current : initial;
|
|
879
|
+
const quantities = s.ticketTiers.filter((row) => row.available && (row.quantity ?? 0) > 0).map((row) => ({ ticketDefinitionId: row.ticketDefinitionId ?? row.id, quantity: row.quantity ?? 0, ...(row.isGuestPrice ? { priceOverride: row.guestPrice } : {}), ...(row.pricingOptionId ? { ticketDetails: [{ pricingOptionId: row.pricingOptionId }] } : {}) }));
|
|
880
|
+
const guestPriceInvalid = s.ticketTiers.some((row) => row.isGuestPrice && (row.quantity ?? 0) > 0 && (!String(row.guestPrice ?? "").trim() || !Number.isFinite(Number(row.guestPrice)) || Number(row.guestPrice) < (row.minPrice ?? 0)));
|
|
881
|
+
const aggregateInvalid = s.ticketTiers.some((row) => {
|
|
882
|
+
const definitionId = row.ticketDefinitionId ?? row.id;
|
|
883
|
+
const definitionQuantity = s.ticketTiers.reduce((sum, other) => sum + ((other.ticketDefinitionId ?? other.id) === definitionId ? other.quantity ?? 0 : 0), 0);
|
|
884
|
+
return (row.quantity ?? 0) > (row.maxQuantity ?? 99) || definitionQuantity > (row.rowLimit ?? 99);
|
|
885
|
+
});
|
|
886
|
+
if (ticketCheckoutInFlight) return derive(s);
|
|
887
|
+
if (quantities.length === 0) return derive({ ...s, snackbarMessage: detailCopy("event.purchaseUnavailable", "Purchasing is temporarily unavailable.") });
|
|
888
|
+
if (guestPriceInvalid || aggregateInvalid || quantities.reduce((sum, row) => sum + row.quantity, 0) > s.ticketLimitPerOrder) return derive(s);
|
|
889
|
+
ticketCheckoutInFlight = true;
|
|
890
|
+
let reservationId = "";
|
|
891
|
+
let result: S | undefined;
|
|
892
|
+
try {
|
|
893
|
+
// Re-read ticket definitions immediately before checkout. A free ticket
|
|
894
|
+
// can become paid (or an IAP ticket) while this screen is open; in that
|
|
895
|
+
// case the regular Events reservation API must not run.
|
|
896
|
+
const latestResponse = await queryAvailableTickets(s.id);
|
|
897
|
+
const latestDefinitions = latestResponse.definitions ?? [];
|
|
898
|
+
const fresh = loadedTiers(latestDefinitions);
|
|
899
|
+
const latestTickets = fresh.tickets;
|
|
900
|
+
const previousSelected = s.ticketTiers.filter((row) => (row.quantity ?? 0) > 0);
|
|
901
|
+
const previousRows = new Map(s.ticketTiers.map((row) => [row.id, row]));
|
|
902
|
+
const refreshedByDefinition = new Map<string, number>();
|
|
903
|
+
let refreshedCount = 0;
|
|
904
|
+
const refreshedRows = fresh.rows.map((row) => {
|
|
905
|
+
const previous = previousRows.get(row.id);
|
|
906
|
+
const definitionId = row.ticketDefinitionId ?? row.id;
|
|
907
|
+
const rowLimit = Math.min(row.rowLimit ?? 99, s.ticketLimitPerOrder);
|
|
908
|
+
const used = refreshedByDefinition.get(definitionId) ?? 0;
|
|
909
|
+
const quantity = row.available ? Math.max(0, Math.min(previous?.quantity ?? 0, rowLimit - used, s.ticketLimitPerOrder - refreshedCount)) : 0;
|
|
910
|
+
refreshedByDefinition.set(definitionId, used + quantity);
|
|
911
|
+
refreshedCount += quantity;
|
|
912
|
+
return { ...row, rowLimit,
|
|
913
|
+
quantity, guestPrice: previous?.guestPrice ?? "",
|
|
914
|
+
priceAmount: row.isGuestPrice ? Number(previous?.guestPrice || row.minPrice || 0) : row.priceAmount,
|
|
915
|
+
isSelected: row.id === s.selectedTierId, notSelected: row.id !== s.selectedTierId };
|
|
916
|
+
});
|
|
917
|
+
const termsChanged = previousSelected.some((previous) => {
|
|
918
|
+
const row = refreshedRows.find((next) => next.id === previous.id);
|
|
919
|
+
return !row || row.priceAmount !== previous.priceAmount || row.minPrice !== previous.minPrice
|
|
920
|
+
|| row.isGuestPrice !== previous.isGuestPrice || row.available !== previous.available
|
|
921
|
+
|| row.rowLimit !== previous.rowLimit || row.quantity !== previous.quantity
|
|
922
|
+
|| latestTickets.get(previous.ticketDefinitionId ?? previous.id)?.price?.currency !== paymentTickets.get(previous.ticketDefinitionId ?? previous.id)?.price?.currency;
|
|
923
|
+
});
|
|
924
|
+
const current = svc.getState();
|
|
925
|
+
s = { ...s, ...(current.status === "ready" && current.id === s.id ? current : {}), ticketTiers: refreshedRows,
|
|
926
|
+
ticketCurrency: latestTickets.values().next().value?.price?.currency || s.ticketCurrency };
|
|
927
|
+
const selectedRows = refreshedRows.filter((row) => (row.quantity ?? 0) > 0);
|
|
928
|
+
const latestSelected = selectedRows.map((row) => selectedPaymentTicket(row, latestTickets));
|
|
929
|
+
const routesAllowed = selectedRows.length === previousSelected.length
|
|
930
|
+
&& latestSelected.every((ticket) => !!ticket && tierAvailable(ticket))
|
|
931
|
+
&& (await Promise.all(latestSelected.map((ticket) => mayUseRegularTicketCheckout(paymentEvent, ticket)))).every(Boolean);
|
|
932
|
+
if (!routesAllowed) {
|
|
933
|
+
paymentTickets = latestTickets;
|
|
934
|
+
ticketCheckoutAllowed = false;
|
|
935
|
+
ticketCheckoutDenied = true;
|
|
936
|
+
if (selectedRows.length === 1 && selectedRows[0]!.quantity === 1) {
|
|
937
|
+
await priceTicket(paymentEvent, selectedRows[0]!.ticketDefinitionId ?? selectedRows[0]!.id, ticketSelectionVersion, latestSelected[0]);
|
|
938
|
+
}
|
|
939
|
+
const current = svc.getState();
|
|
940
|
+
const latest = current.status === "ready" && current.id === s.id ? { ...s, ...current } : s;
|
|
941
|
+
result = derive({ ...latest, ticketTiers: refreshedRows, ticketCurrency: s.ticketCurrency, snackbarMessage: copyParam("event.purchaseUnavailable", "Purchasing is temporarily unavailable.") });
|
|
942
|
+
ticketSelectionState = result;
|
|
943
|
+
if (result.status === "ready") lastReady = result;
|
|
944
|
+
} else {
|
|
945
|
+
paymentTickets = latestTickets;
|
|
946
|
+
ticketCheckoutAllowed = true;
|
|
947
|
+
ticketCheckoutDenied = false;
|
|
948
|
+
if (termsChanged) {
|
|
949
|
+
ticketCheckoutInFlight = false;
|
|
950
|
+
result = derive({ ...s, snackbarMessage: detailCopy("event.ticketsChanged", "Ticket details changed. Review your selection and try again.") });
|
|
951
|
+
ticketSelectionState = result;
|
|
952
|
+
lastReady = result;
|
|
953
|
+
} else if (isHostedWorker()) {
|
|
954
|
+
if (hostedOpenInFlight) result = derive(svc.getState());
|
|
955
|
+
else {
|
|
956
|
+
hostedOpenInFlight = true;
|
|
957
|
+
try {
|
|
958
|
+
const opened = await hostedSitePageOpener({ page: "event", eventId: s.id })(async () => {
|
|
959
|
+
try {
|
|
960
|
+
const fresh = await queryAvailableTickets(s.id);
|
|
961
|
+
const byId = new Map((fresh.definitions ?? []).filter((ticket) => ticket.id).map((ticket) => [ticket.id!, ticket]));
|
|
962
|
+
const freshSelected = selectedRows.map((row) => selectedPaymentTicket(row, byId));
|
|
963
|
+
const stillAllowed = freshSelected.every((ticket) => !!ticket && tierAvailable(ticket))
|
|
964
|
+
&& (await Promise.all(freshSelected.map((ticket) => mayUseRegularTicketCheckout(paymentEvent, ticket)))).every(Boolean);
|
|
965
|
+
if (stillAllowed) paymentTickets = byId;
|
|
966
|
+
return stillAllowed;
|
|
967
|
+
} catch { return false; }
|
|
968
|
+
});
|
|
969
|
+
const latestServiceState = svc.getState();
|
|
970
|
+
const latest = latestServiceState.status === "ready" && latestServiceState.id === s.id ? { ...s, ...latestServiceState } : s;
|
|
971
|
+
result = opened === true
|
|
972
|
+
? { ...latest, ticketSheetOpen: false, snackbarMessage: detailCopy("event.checkoutOpened", "Complete checkout in the browser to get your tickets.") }
|
|
973
|
+
: opened === "withdrawn" ? latest : { ...latest, snackbarMessage: detailCopy("event.registrationOpenFailed", "Could not open the registration page.") };
|
|
974
|
+
} finally { hostedOpenInFlight = false; }
|
|
975
|
+
}
|
|
976
|
+
} else {
|
|
977
|
+
const held = await createTicketReservation(s.id, quantities);
|
|
978
|
+
if (!held.id) throw new Error("Reservation did not return an id");
|
|
979
|
+
reservationId = held.id;
|
|
980
|
+
const placed = await checkoutReservation(s.id, held.id);
|
|
981
|
+
const snapshotId = placed.order?.snapshotId ?? "";
|
|
982
|
+
if (!snapshotId) result = { ...s, ticketSheetOpen: false, snackbarMessage: detailCopy("event.ticketIssued", "You're in — your ticket is confirmed.") };
|
|
983
|
+
else {
|
|
984
|
+
const msid = await wixPlatformSession.signedInstanceStatus().then((st) => st.msid ?? "").catch(() => "");
|
|
985
|
+
const pay = await payCashierOrder({ appId: WIX_EVENTS_APP_ID, snapshotId, msid, amount: placed.order?.totalPrice?.amount, currency: placed.order?.totalPrice?.currency, cashierUrl: async () => (await createRedirectSession({ eventsCheckout: { reservationId: held.id! } })).redirectSession?.fullUrl });
|
|
986
|
+
if (pay.kind === "paid") result = { ...s, ticketSheetOpen: false, snackbarMessage: detailCopy("event.ticketIssued", "You're in — your ticket is confirmed.") };
|
|
987
|
+
else if (pay.kind === "handoff") result = { ...s, ticketSheetOpen: false, snackbarMessage: detailCopy("event.checkoutOpened", "Complete checkout in the browser to get your tickets.") };
|
|
988
|
+
else if (pay.kind === "cancelled") result = { ...s, snackbarMessage: detailCopy("event.ticketCancelled", "Payment cancelled — no ticket was issued.") };
|
|
989
|
+
else throw new Error("Payment could not be completed");
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
} catch {
|
|
994
|
+
try {
|
|
995
|
+
// A public event page is not pinned to this selection or price. Only
|
|
996
|
+
// continue in hosted checkout when a reservation already holds it.
|
|
997
|
+
const params = reservationId ? { reservationId } : undefined;
|
|
998
|
+
if (params) {
|
|
999
|
+
const redirect = await createRedirectSession({ eventsCheckout: params });
|
|
1000
|
+
const url = redirect.redirectSession?.fullUrl;
|
|
1001
|
+
if (url) {
|
|
1002
|
+
const opened = await open(url, "in_app_browser");
|
|
1003
|
+
result = { ...s, ticketSheetOpen: false, snackbarMessage: opened.opened ? detailCopy("event.checkoutOpened", "Complete checkout in the browser to get your tickets.") : detailCopy("event.registrationOpenFailed", "Could not open the registration page.") };
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
} catch { /* report below */ }
|
|
1007
|
+
if (!result) result = { ...s, snackbarMessage: detailCopy("event.purchaseUnavailable", "Purchasing is temporarily unavailable.") };
|
|
1008
|
+
} finally { ticketCheckoutInFlight = false; }
|
|
1009
|
+
return derive(result!);
|
|
1010
|
+
},
|
|
1011
|
+
iapPublish: (s) => {
|
|
1012
|
+
// Prefer the live ready state so an asynchronous StoreKit publication
|
|
1013
|
+
// cannot overwrite a newer selection or checkout snackbar with the
|
|
1014
|
+
// original load snapshot.
|
|
1015
|
+
const next = derive(s.status === "ready" ? s : lastReady ?? s);
|
|
1016
|
+
if (next.status === "ready") lastReady = next;
|
|
1017
|
+
return next;
|
|
1018
|
+
},
|
|
513
1019
|
iapRefresh: async (): Promise<S> => { await applePurchase.refresh(); return derive(svc.getState()); },
|
|
514
1020
|
iapPoll: async (): Promise<S> => { await applePurchase.poll(); return derive(svc.getState()); },
|
|
515
1021
|
setIapField: (s, arg) => { applePurchase.change(arg); return derive(s); },
|
|
516
1022
|
buyApple: async (s: S): Promise<S> => {
|
|
517
|
-
|
|
518
|
-
|
|
1023
|
+
const selected = s.ticketTiers.find((t) => t.id === s.selectedTierId);
|
|
1024
|
+
if (!selected || !selected.available || selected.quantity !== 1 || s.ticketTiers.some((t) => t.id !== s.selectedTierId && (t.quantity ?? 0) > 0)) return derive(s);
|
|
1025
|
+
await applePurchase.buy({ key: selected.ticketDefinitionId ?? selected.id, eventId: s.id });
|
|
519
1026
|
return derive(svc.getState());
|
|
520
1027
|
},
|
|
521
1028
|
load: async (s: S, arg?: unknown): Promise<S | undefined> => {
|
|
522
1029
|
const selectionVersion = ++ticketSelectionVersion;
|
|
523
1030
|
ticketCheckoutAllowed = false;
|
|
1031
|
+
ticketCheckoutDenied = false;
|
|
1032
|
+
ticketSelectionState = undefined;
|
|
1033
|
+
lastReady = undefined;
|
|
1034
|
+
s = { ...s, ticketSheetOpen: false, rsvpActionsOpen: false, cancelConfirmOpen: false, rsvpId: "", rsvpState: "none", rsvpLabel: "" };
|
|
524
1035
|
applePurchase.invalidate();
|
|
525
1036
|
stashCopy(arg);
|
|
526
1037
|
const id = s.id || inputId();
|
|
@@ -531,72 +1042,164 @@ const svc = defineService<S, {
|
|
|
531
1042
|
// URLS adds `eventPageUrl` — the registration fallback and the share link. Without URLS a
|
|
532
1043
|
// ticket flow that cannot complete in-app had no page to send the buyer to.
|
|
533
1044
|
const res = await getEvent(id, { fields: EVENT_DETAIL_FIELDSETS });
|
|
534
|
-
|
|
535
|
-
e = await enrichRegistration(id, e);
|
|
536
|
-
const attendees = await attendeeSummary(id);
|
|
1045
|
+
const e = ((res as unknown as { event?: WixEvent }).event ?? res) as WixEvent;
|
|
537
1046
|
// Ticketed events load their tiers with the event, so the price is on
|
|
538
1047
|
// screen BEFORE the CTA is tapped. Best-effort: a tier read that fails
|
|
539
1048
|
// leaves the picker collapsed and the CTA falls back to its own read.
|
|
540
1049
|
const kind = registrationKindOf(e);
|
|
541
1050
|
const loaded = kind === "tickets" ? await loadTiers(id) : { rows: [], tickets: new Map<string, WixAvailableTicket>() };
|
|
542
|
-
const
|
|
1051
|
+
const orderLimit = Number((e as unknown as { registration?: { tickets?: { ticketLimitPerOrder?: number } } }).registration?.tickets?.ticketLimitPerOrder ?? 99);
|
|
1052
|
+
const tiers = loaded.rows.map((row) => ({ ...row, rowLimit: Math.min(row.rowLimit ?? 99, orderLimit), maxQuantity: Math.min(row.maxQuantity ?? 99, orderLimit) }));
|
|
543
1053
|
const selectedTierId = tiers.find((t) => t.isSelected)?.id ?? tiers.find((t) => t.available)?.id ?? "";
|
|
544
|
-
const
|
|
1054
|
+
const selectedTicketId = tiers.find((row) => row.id === selectedTierId)?.ticketDefinitionId ?? selectedTierId;
|
|
1055
|
+
const regular = kind !== "tickets" || await mayUseRegularTicketCheckout(e, loaded.tickets.get(selectedTicketId));
|
|
545
1056
|
if (selectionVersion !== ticketSelectionVersion) return undefined;
|
|
546
1057
|
paymentEvent = e;
|
|
547
1058
|
paymentTickets = loaded.tickets;
|
|
548
1059
|
ticketCheckoutAllowed = regular;
|
|
549
|
-
if (kind === "tickets" && !await priceTicket(e,
|
|
1060
|
+
if (kind === "tickets" && !await priceTicket(e, selectedTicketId, selectionVersion)) return undefined;
|
|
550
1061
|
// Write-through: cache the fetched row (same EventRow shape as the list) so a
|
|
551
1062
|
// later offline open of this detail can fall back to it.
|
|
552
1063
|
await writeCachedRow(normalizeEvent(e));
|
|
553
1064
|
const image = eventImage(e);
|
|
554
|
-
const title = e.title || "
|
|
555
|
-
|
|
1065
|
+
const title = e.title || "";
|
|
1066
|
+
const dates = detailDate(e);
|
|
1067
|
+
const place = detailPlace(e);
|
|
1068
|
+
const registration = registrationUi(e, Date.now());
|
|
1069
|
+
const rawEvent = e as unknown as { shortDescription?: string; agendaSettings?: { enabled?: boolean }; dateAndTimeSettings?: { recurrenceStatus?: string; recurringEvents?: { categoryId?: string } }; registration?: { allowedGuestTypes?: string; tickets?: { lowestPrice?: EventDetailMoney; highestPrice?: EventDetailMoney } } };
|
|
1070
|
+
const lowest = detailMoney(rawEvent.registration?.tickets?.lowestPrice);
|
|
1071
|
+
const highest = detailMoney(rawEvent.registration?.tickets?.highestPrice);
|
|
1072
|
+
const price = kind === "tickets" && lowest ? (highest && highest !== lowest ? detailCopy("event.priceFrom", "From {price}").replace("{price}", lowest) : lowest) : "";
|
|
1073
|
+
const memberProbeNeeded = kind === "rsvp" || rawEvent.registration?.allowedGuestTypes === "MEMBER";
|
|
1074
|
+
const isMember = memberProbeNeeded ? await signedInMember() : false;
|
|
1075
|
+
const [scheduleResponse, policyResponse, rsvpResponse] = await Promise.all([
|
|
1076
|
+
rawEvent.agendaSettings?.enabled ? bestEffort("GET", `/events/v1/schedule?eventId=${encodeURIComponent(id)}`) : undefined,
|
|
1077
|
+
bestEffort("POST", "/events-policies/v2/policies/query", { query: { filter: { eventId: { $eq: id } }, paging: { limit: 100 } } }),
|
|
1078
|
+
kind === "rsvp" && isMember ? bestEffort("GET", `/events/v2/rsvps/current?eventId=${encodeURIComponent(id)}`) : undefined,
|
|
1079
|
+
]);
|
|
1080
|
+
let optionalReadFailed = [scheduleResponse, policyResponse, rsvpResponse].some((response) => response?.ok === false);
|
|
1081
|
+
const scheduleRaw = (scheduleResponse?.ok ? scheduleResponse.value : undefined) as { agenda?: Array<Record<string, unknown>>; schedule?: Array<Record<string, unknown>>; items?: Array<Record<string, unknown>>; agendaItems?: Array<Record<string, unknown>> } | undefined;
|
|
1082
|
+
const fullSchedule = (scheduleRaw?.agenda ?? scheduleRaw?.agendaItems ?? scheduleRaw?.schedule ?? scheduleRaw?.items ?? []).map((item) => {
|
|
1083
|
+
const slot = item.timeSlot as { start?: string; end?: string; timeZoneId?: string } | undefined;
|
|
1084
|
+
const start = String(slot?.start ?? item.startTime ?? item.startDate ?? item.time ?? "");
|
|
1085
|
+
const date = start ? new Date(start) : undefined;
|
|
1086
|
+
return {
|
|
1087
|
+
time: date && !Number.isNaN(date.getTime()) ? formatWeekdayDateTime(date, "en-US", slot?.timeZoneId) : start,
|
|
1088
|
+
name: String(item.name ?? item.title ?? ""),
|
|
1089
|
+
detail: String(item.stageName ?? item.stage ?? item.host ?? ""),
|
|
1090
|
+
};
|
|
1091
|
+
});
|
|
1092
|
+
const scheduleItems = fullSchedule.slice(0, 3);
|
|
1093
|
+
const rawPolicies = (((policyResponse?.ok ? policyResponse.value : undefined) as { policies?: Array<Record<string, unknown>> } | undefined)?.policies ?? []);
|
|
1094
|
+
const policies = rawPolicies.map((policy) => ({ name: String(policy.name ?? ""), text: String(policy.body ?? "") })).filter((policy) => policy.name || policy.text);
|
|
1095
|
+
const rsvpPayload = (rsvpResponse?.ok ? rsvpResponse.value : undefined) as { rsvps?: Array<{ id?: string; status?: string }>; rsvp?: { id?: string; status?: string } } | undefined;
|
|
1096
|
+
const rsvps = rsvpPayload?.rsvps ?? (rsvpPayload?.rsvp ? [rsvpPayload.rsvp] : []);
|
|
1097
|
+
const heldRsvp = rsvps.find((r) => ["YES", "WAITLIST", "WAITING"].includes((r.status ?? "").toUpperCase()));
|
|
1098
|
+
const rsvpState = !heldRsvp ? "none" : /(WAITLIST|WAITING)/i.test(heldRsvp.status ?? "") ? "waitlist" : "going";
|
|
1099
|
+
const rsvpLabel = rsvpState === "waitlist" ? detailCopy("event.waitlisted", "You're on the waitlist") : rsvpState === "going" ? detailCopy("event.going", "You're going") : "";
|
|
1100
|
+
const memberOnly = rawEvent.registration?.allowedGuestTypes === "MEMBER" && !isMember && ["rsvp", "waitlist", "tickets", "scheduled"].includes(registration.state);
|
|
1101
|
+
const registrationRaw = rawEvent.registration as { status?: string; registrationPaused?: boolean; tickets?: { soldOut?: boolean } } | undefined;
|
|
1102
|
+
const registrationClosed = !!registrationRaw?.registrationPaused || ["CLOSED_AUTOMATICALLY", "CLOSED_MANUALLY"].includes(registrationRaw?.status ?? "");
|
|
1103
|
+
const soldOut = kind === "tickets" && !!registrationRaw?.tickets?.soldOut;
|
|
1104
|
+
const formMessages = (e as unknown as { form?: { messages?: { registrationClosed?: { exploreEventsActionLabel?: string }; ticketsUnavailable?: { exploreEventsActionLabel?: string } } } }).form?.messages;
|
|
1105
|
+
const exploreLabel = registrationClosed
|
|
1106
|
+
? formMessages?.registrationClosed?.exploreEventsActionLabel
|
|
1107
|
+
: soldOut ? formMessages?.ticketsUnavailable?.exploreEventsActionLabel : undefined;
|
|
1108
|
+
let moreDates: Array<{ id: string; date: string }> = [];
|
|
1109
|
+
const recurrence = rawEvent.dateAndTimeSettings?.recurringEvents;
|
|
1110
|
+
if (recurrence?.categoryId && rawEvent.dateAndTimeSettings?.recurrenceStatus !== "ONE_TIME") {
|
|
1111
|
+
try {
|
|
1112
|
+
const series: WixEvent[] = [];
|
|
1113
|
+
// The Events V3 category endpoint is the supported scoped series read.
|
|
1114
|
+
// Page in bounded batches, since a single 100-row response can omit later dates.
|
|
1115
|
+
for (let offset = 0; offset < 1000; offset += 100) {
|
|
1116
|
+
const page = await bestEffort("GET", `/events/v3/events/category/${encodeURIComponent(recurrence.categoryId)}?paging.limit=100&paging.offset=${offset}&fields=DETAILS&fields=TEXTS&fields=REGISTRATION`);
|
|
1117
|
+
if (!page.ok) { optionalReadFailed = true; break; }
|
|
1118
|
+
const pageEvents = (page.value.events ?? []) as WixEvent[];
|
|
1119
|
+
series.push(...pageEvents);
|
|
1120
|
+
if (pageEvents.length < 100) break;
|
|
1121
|
+
}
|
|
1122
|
+
const now = Date.now();
|
|
1123
|
+
moreDates = series.filter((item) => {
|
|
1124
|
+
const itemRaw = item as unknown as { status?: string; dateAndTimeSettings?: { recurringEvents?: { categoryId?: string } } };
|
|
1125
|
+
const start = eventStartMillis(item);
|
|
1126
|
+
return item.id !== id && itemRaw.dateAndTimeSettings?.recurringEvents?.categoryId === recurrence.categoryId && !["ENDED", "CANCELED", "DRAFT"].includes((itemRaw.status ?? "").toUpperCase()) && (start == null || start >= now);
|
|
1127
|
+
}).sort((a, b) => (eventStartMillis(a) ?? 0) - (eventStartMillis(b) ?? 0)).map((item) => ({ id: item.id ?? "", date: eventWhen(item) })).filter((item) => !!item.id);
|
|
1128
|
+
} catch { moreDates = []; optionalReadFailed = true; }
|
|
1129
|
+
}
|
|
1130
|
+
const readyState = derive({
|
|
556
1131
|
...s,
|
|
557
1132
|
status: "ready",
|
|
558
1133
|
errored: false,
|
|
559
1134
|
id,
|
|
560
1135
|
ticketTiers: tiers,
|
|
1136
|
+
ticketCurrency: (e as unknown as { registration?: { tickets?: { currency?: string } } }).registration?.tickets?.currency || loaded.tickets.values().next().value?.price?.currency || "USD",
|
|
1137
|
+
ticketLimitPerOrder: orderLimit,
|
|
561
1138
|
// Default the pick to the cheapest AVAILABLE tier so the CTA is never
|
|
562
1139
|
// dead; the buyer's tap overrides it from there.
|
|
563
1140
|
selectedTierId,
|
|
564
1141
|
title,
|
|
565
|
-
|
|
566
|
-
when: eventWhen(e),
|
|
1142
|
+
when: dates.line,
|
|
567
1143
|
location: eventLocation(e),
|
|
1144
|
+
place: place.value,
|
|
1145
|
+
placeIcon: place.icon,
|
|
1146
|
+
noPlace: !place.value,
|
|
1147
|
+
hidePlaceStatic: !place.value || !!place.mapUrl,
|
|
1148
|
+
price,
|
|
1149
|
+
zone: dates.zone,
|
|
1150
|
+
mapUrl: place.mapUrl,
|
|
1151
|
+
shortDescription: rawEvent.shortDescription || "",
|
|
1152
|
+
noShortDescription: !rawEvent.shortDescription || rawEvent.shortDescription === eventBody(e).description,
|
|
1153
|
+
noDescription: !eventBody(e).description && !eventBody(e).descriptionJson,
|
|
1154
|
+
noDate: !dates.line,
|
|
1155
|
+
registrationState: registration.state,
|
|
1156
|
+
footerLabel: registration.label,
|
|
1157
|
+
footerAction: registration.action,
|
|
1158
|
+
footerCaption: memberOnly ? (kind === "tickets" ? detailCopy("event.membersOnlyTickets", "Only members can buy tickets.") : detailCopy("event.membersOnlyRsvp", "Only members can RSVP.")) : registration.caption,
|
|
1159
|
+
memberOnly,
|
|
1160
|
+
notice: registration.notice,
|
|
1161
|
+
exploreLabel: exploreLabel || detailCopy("event.exploreCta", "Explore Events"),
|
|
1162
|
+
rsvpLabel,
|
|
1163
|
+
rsvpId: heldRsvp?.id ?? "",
|
|
1164
|
+
rsvpState,
|
|
1165
|
+
schedule: scheduleItems,
|
|
1166
|
+
fullSchedule: fullSchedule.slice(3),
|
|
1167
|
+
noSchedule: scheduleItems.length === 0,
|
|
1168
|
+
policies,
|
|
1169
|
+
noPolicies: policies.length === 0,
|
|
1170
|
+
moreDates: moreDates.slice(0, 3),
|
|
1171
|
+
allDates: moreDates.slice(3),
|
|
1172
|
+
noMoreDates: moreDates.length === 0,
|
|
568
1173
|
// The Ricos description when the event has one (its own text is the plain copy); the legacy
|
|
569
1174
|
// string fields otherwise.
|
|
570
1175
|
...eventBody(e),
|
|
571
1176
|
image,
|
|
572
1177
|
hasImage: !!image,
|
|
573
1178
|
noImage: !image,
|
|
574
|
-
|
|
575
|
-
mapMarkers: eventMapMarkers(e),
|
|
576
|
-
goingLabel: attendees.goingLabel,
|
|
577
|
-
attendeeAvatars: attendees.avatars,
|
|
1179
|
+
hideMapTap: place.hideMapTap,
|
|
578
1180
|
registrationKind: registrationKindOf(e),
|
|
579
1181
|
registrationUrl: registrationUrlOf(e),
|
|
580
1182
|
eventSlug: eventSlugOf(e),
|
|
581
1183
|
shareUrl: eventPageUrlOf(e),
|
|
582
|
-
calendarStart:
|
|
583
|
-
calendarEnd:
|
|
584
|
-
|
|
585
|
-
statusLabel: titleCase(e.eventStatus) || titleCase((e as unknown as { status?: string }).status),
|
|
1184
|
+
calendarStart: dates.start,
|
|
1185
|
+
calendarEnd: dates.end,
|
|
1186
|
+
snackbarMessage: optionalReadFailed ? detailCopy("event.detailsUnavailable", "Some event details couldn't load. Try again.") : "",
|
|
586
1187
|
});
|
|
587
|
-
|
|
1188
|
+
ticketSelectionState = readyState;
|
|
1189
|
+
lastReady = readyState;
|
|
1190
|
+
return readyState;
|
|
1191
|
+
} catch (error) {
|
|
588
1192
|
// Offline / fetch failure: fall back to the last-synced cached row before erroring.
|
|
589
1193
|
if (selectionVersion !== ticketSelectionVersion) return undefined;
|
|
590
1194
|
const cached = await readCachedRow(id);
|
|
591
1195
|
if (cached) {
|
|
592
1196
|
const title = cached.title || "Event";
|
|
593
|
-
|
|
1197
|
+
const cachedState = derive({
|
|
594
1198
|
...s,
|
|
595
1199
|
status: "ready",
|
|
596
1200
|
errored: false,
|
|
597
1201
|
id,
|
|
598
1202
|
title,
|
|
599
|
-
initial: (title.trim()[0] || "E").toUpperCase(),
|
|
600
1203
|
when: cached.dateLine,
|
|
601
1204
|
location: cached.location,
|
|
602
1205
|
// The cached list row carries no description: an offline retry of THIS event keeps the pair it
|
|
@@ -605,193 +1208,31 @@ const svc = defineService<S, {
|
|
|
605
1208
|
image: cached.image,
|
|
606
1209
|
hasImage: !!cached.image,
|
|
607
1210
|
noImage: !cached.image,
|
|
608
|
-
statusLabel: titleCase(cached.status),
|
|
609
1211
|
});
|
|
1212
|
+
lastReady = cachedState;
|
|
1213
|
+
return cachedState;
|
|
610
1214
|
}
|
|
611
|
-
|
|
1215
|
+
const errorState = derive({ ...s, status: "ready", errored: true, title: "" });
|
|
1216
|
+
lastReady = errorState;
|
|
1217
|
+
return errorState;
|
|
612
1218
|
}
|
|
613
1219
|
},
|
|
614
1220
|
openMap: async (s) => {
|
|
615
|
-
if (!s.mapUrl) return derive({ ...s,
|
|
1221
|
+
if (!s.mapUrl) return derive({ ...s, snackbarMessage: copyParam("event.noMapMessage", "This event does not have a mapped location yet.") });
|
|
616
1222
|
try {
|
|
617
1223
|
const result = await open(s.mapUrl, "external_app");
|
|
618
|
-
return derive({ ...s,
|
|
1224
|
+
return derive({ ...s, snackbarMessage: result.opened ? "" : copyParam("event.mapOpenFailed", "Could not open maps on this device.") });
|
|
619
1225
|
} catch {
|
|
620
|
-
return derive({ ...s,
|
|
1226
|
+
return derive({ ...s, snackbarMessage: copyParam("event.mapOpenFailed", "Could not open maps on this device.") });
|
|
621
1227
|
}
|
|
622
1228
|
},
|
|
623
|
-
selectTier: async (s: S, arg?: unknown): Promise<S | undefined> => {
|
|
624
|
-
const id = typeof arg === "string" ? arg : (arg as { id?: unknown } | undefined)?.id;
|
|
625
|
-
const wanted = typeof id === "string" ? id : "";
|
|
626
|
-
const tier = s.ticketTiers.find((t) => t.id === wanted);
|
|
627
|
-
// Ignore a tap on an off-sale row rather than letting the CTA promise a
|
|
628
|
-
// ticket it cannot reserve.
|
|
629
|
-
if (!tier || !tier.available) return s;
|
|
630
|
-
const selectionVersion = ++ticketSelectionVersion;
|
|
631
|
-
ticketCheckoutAllowed = false;
|
|
632
|
-
applePurchase.invalidate();
|
|
633
|
-
const regular = await mayUseRegularTicketCheckout(paymentEvent, paymentTickets.get(wanted));
|
|
634
|
-
if (selectionVersion !== ticketSelectionVersion) return undefined;
|
|
635
|
-
ticketCheckoutAllowed = regular;
|
|
636
|
-
if (!await priceTicket(paymentEvent, wanted, selectionVersion)) return undefined;
|
|
637
|
-
return derive({
|
|
638
|
-
...s,
|
|
639
|
-
selectedTierId: wanted,
|
|
640
|
-
ticketTiers: s.ticketTiers.map((t) => ({ ...t, isSelected: t.id === wanted, notSelected: t.id !== wanted })),
|
|
641
|
-
// A new pick invalidates whatever the last attempt said.
|
|
642
|
-
actionMessage: ticketCheckoutAllowed || !applePurchase.fields(true, false).iapBuyHidden ? "" : copyParam("event.purchaseUnavailable", "Purchasing is temporarily unavailable."),
|
|
643
|
-
});
|
|
644
|
-
},
|
|
645
1229
|
openRegistration: async (s): Promise<S> => {
|
|
646
|
-
|
|
647
|
-
let checkoutEvent: WixEvent | undefined;
|
|
648
|
-
if (s.registrationKind === "tickets") {
|
|
649
|
-
try {
|
|
650
|
-
const current = await getEvent(s.id, { fields: EVENT_DETAIL_FIELDSETS });
|
|
651
|
-
const event = ((current as unknown as { event?: WixEvent }).event ?? current) as WixEvent;
|
|
652
|
-
checkoutEvent = event;
|
|
653
|
-
eventPageAllowed = await mayUseRegularTicketCheckout(event, undefined);
|
|
654
|
-
ticketCheckoutAllowed = eventPageAllowed;
|
|
655
|
-
if (!ticketCheckoutAllowed) {
|
|
656
|
-
const tickets = await queryAvailableTickets(s.id);
|
|
657
|
-
const selected = (tickets.definitions ?? []).find((ticket) => ticket.id === s.selectedTierId);
|
|
658
|
-
ticketCheckoutAllowed = await mayUseRegularTicketCheckout(event, selected);
|
|
659
|
-
}
|
|
660
|
-
if (!ticketCheckoutAllowed) return derive({ ...s, actionMessage: copyParam("event.purchaseUnavailable", "Purchasing is temporarily unavailable.") });
|
|
661
|
-
} catch { return derive({ ...s, actionMessage: copyParam("event.purchaseUnavailable", "Purchasing is temporarily unavailable.") }); }
|
|
662
|
-
}
|
|
663
|
-
// Phase 2c — NATIVE ticket purchase. Reserve the cheapest available tier,
|
|
664
|
-
// turn the hold into an order, then settle it on the shared in-app pay rail
|
|
665
|
-
// (@wix/himalaya/checkout): wallet where the app has one, in-app Wix cashier
|
|
666
|
-
// otherwise. A FREE ticket comes back with no `snapshotId` and is already
|
|
667
|
-
// confirmed — no payment step at all, which the hosted redirect made
|
|
668
|
-
// needlessly heavy.
|
|
669
|
-
//
|
|
670
|
-
// Every failure degrades one rung at a time, never to a dead button:
|
|
671
|
-
// native chain → hosted eventsCheckout (pinned to the SAME reservation
|
|
672
|
-
// when we got one, else the slug) → the event page / registration URL.
|
|
673
|
-
// Hosted inside a host shell (Spaces) neither in-app rung can finish: the classic
|
|
674
|
-
// `checkoutReservation` needs a buyer + guest form this screen does not collect, and the
|
|
675
|
-
// redirect session needs a headless OAuth client a hosted app does not have. Skip both —
|
|
676
|
-
// the native chain would also leave a ticket reservation held for nothing — and open the
|
|
677
|
-
// event's own page, where the member registers on the site: signed in by the host where it can
|
|
678
|
-
// (`himi.host.memberSitePage`), else as a visitor.
|
|
679
|
-
if (s.registrationKind === "tickets" && s.id && !isHostedWorker()) {
|
|
680
|
-
let reservationId = "";
|
|
681
|
-
try {
|
|
682
|
-
// Buy what the buyer PICKED. `load` seeds the pick with the cheapest
|
|
683
|
-
// available tier so the CTA is never dead, and `selectTier` follows the
|
|
684
|
-
// tap after that — silently charging for the cheapest tier when a buyer
|
|
685
|
-
// had chosen another one would be the wrong kind of "helpful".
|
|
686
|
-
//
|
|
687
|
-
// The re-read is deliberate: the tier list on screen may be seconds or
|
|
688
|
-
// minutes old, and a tier can sell out in between. Re-reading also
|
|
689
|
-
// covers the case where `load`'s best-effort tier read failed and the
|
|
690
|
-
// picker is collapsed.
|
|
691
|
-
const tickets = await queryAvailableTickets(s.id);
|
|
692
|
-
const live = (tickets.definitions ?? []).filter(tierAvailable);
|
|
693
|
-
// A recovered first read must show the offer before it can be sold.
|
|
694
|
-
if (live.length > 0 && !s.selectedTierId && s.ticketTiers.length === 0) return svc.load();
|
|
695
|
-
const tier = live.find((t) => t.id === s.selectedTierId);
|
|
696
|
-
if (!tier) return derive({ ...s, actionMessage: copyParam("event.ticketChanged", "That ticket is no longer available. Choose a ticket again.") });
|
|
697
|
-
ticketCheckoutAllowed = await mayUseRegularTicketCheckout(checkoutEvent, tier);
|
|
698
|
-
if (!ticketCheckoutAllowed) return derive({ ...s, actionMessage: copyParam("event.purchaseUnavailable", "Purchasing is temporarily unavailable.") });
|
|
699
|
-
if (tier?.id) {
|
|
700
|
-
const held = await createTicketReservation(s.id, [{ ticketDefinitionId: tier.id, quantity: 1 }]);
|
|
701
|
-
reservationId = held.id ?? "";
|
|
702
|
-
if (reservationId) {
|
|
703
|
-
const placed = await checkoutReservation(s.id, reservationId);
|
|
704
|
-
const order = placed.order;
|
|
705
|
-
const snapshotId = order?.snapshotId ?? "";
|
|
706
|
-
if (!snapshotId) {
|
|
707
|
-
// Nothing to charge ⇒ the ticket is already issued.
|
|
708
|
-
return derive({ ...s, actionMessage: copyParam("event.ticketIssued", "You're in — your ticket is confirmed.") });
|
|
709
|
-
}
|
|
710
|
-
const msid = await wixPlatformSession
|
|
711
|
-
.signedInstanceStatus()
|
|
712
|
-
.then((st) => st.msid ?? "")
|
|
713
|
-
.catch(() => "");
|
|
714
|
-
const pay = await payCashierOrder({
|
|
715
|
-
appId: WIX_EVENTS_APP_ID,
|
|
716
|
-
snapshotId,
|
|
717
|
-
msid,
|
|
718
|
-
amount: order?.totalPrice?.amount,
|
|
719
|
-
currency: order?.totalPrice?.currency,
|
|
720
|
-
// Wallet-less apps keep the hosted events checkout, IN-APP — and
|
|
721
|
-
// pinned to the reservation we already hold, so the buyer doesn't
|
|
722
|
-
// restart tier selection from the slug.
|
|
723
|
-
cashierUrl: async () => {
|
|
724
|
-
const redirect = await createRedirectSession({ eventsCheckout: { reservationId } });
|
|
725
|
-
return redirect.redirectSession?.fullUrl;
|
|
726
|
-
},
|
|
727
|
-
});
|
|
728
|
-
if (pay.kind === "paid") {
|
|
729
|
-
return derive({ ...s, actionMessage: copyParam("event.ticketIssued", "You're in — your ticket is confirmed.") });
|
|
730
|
-
}
|
|
731
|
-
if (pay.kind === "handoff") return derive({ ...s, actionMessage: copyParam("event.checkoutOpened", "Complete checkout in the browser to get your tickets.") });
|
|
732
|
-
if (pay.kind === "cancelled") {
|
|
733
|
-
return derive({ ...s, actionMessage: copyParam("event.ticketCancelled", "Payment cancelled — no ticket was issued.") });
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
} catch {
|
|
738
|
-
// fall through to the hosted rail below
|
|
739
|
-
}
|
|
740
|
-
// Hosted eventsCheckout — pinned to the live reservation when we have one.
|
|
741
|
-
if (!eventPageAllowed && !reservationId) return derive({ ...s, actionMessage: copyParam("event.purchaseUnavailable", "Purchasing is temporarily unavailable.") });
|
|
742
|
-
try {
|
|
743
|
-
const params = reservationId ? { reservationId } : s.eventSlug ? { eventSlug: s.eventSlug } : undefined;
|
|
744
|
-
if (params) {
|
|
745
|
-
const redirect = await createRedirectSession({ eventsCheckout: params });
|
|
746
|
-
const url = redirect.redirectSession?.fullUrl;
|
|
747
|
-
if (url) {
|
|
748
|
-
const result = await open(url, "in_app_browser");
|
|
749
|
-
return derive({
|
|
750
|
-
...s,
|
|
751
|
-
actionMessage: result.opened
|
|
752
|
-
? copyParam("event.checkoutOpened", "Complete checkout in the browser to get your tickets.")
|
|
753
|
-
: copyParam("event.registrationOpenFailed", "Could not open the registration page."),
|
|
754
|
-
});
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
} catch {
|
|
758
|
-
// fall through to registrationUrl
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
// Hosted: the HOST opens the event's own page signed in as the lent member — ids only; it
|
|
762
|
-
// reads the page URL from Wix and refuses (a custom domain → a visitor page; no published Events
|
|
763
|
-
// page → nothing) on its own. Nothing here can change between its prepare and its open (the event
|
|
764
|
-
// is fixed and the page is where the member chooses and pays), so the guard has nothing to re-read.
|
|
765
|
-
// A host that refused or failed leaves the visitor page below.
|
|
766
|
-
if (!eventPageAllowed) return derive({ ...s, actionMessage: copyParam("event.purchaseUnavailable", "Purchasing is temporarily unavailable.") });
|
|
767
|
-
if (isHostedWorker() && s.id && (s.registrationKind === "tickets" || s.registrationKind === "rsvp")) {
|
|
768
|
-
// One signed-in open at a time: the host keeps a single one-time ticket, so a second tap would
|
|
769
|
-
// replace the first tap's ticket and drop that tap into the signed-OUT visitor page below.
|
|
770
|
-
if (hostedOpenInFlight) return s;
|
|
771
|
-
hostedOpenInFlight = true;
|
|
772
|
-
let opened: boolean | "withdrawn";
|
|
773
|
-
try {
|
|
774
|
-
opened = await hostedSitePageOpener({ page: "event", eventId: s.id })(async () => true);
|
|
775
|
-
} finally {
|
|
776
|
-
hostedOpenInFlight = false;
|
|
777
|
-
}
|
|
778
|
-
if (opened === true) {
|
|
779
|
-
return derive({
|
|
780
|
-
...s,
|
|
781
|
-
actionMessage: s.registrationKind === "tickets"
|
|
782
|
-
? copyParam("event.checkoutOpened", "Complete checkout in the browser to get your tickets.")
|
|
783
|
-
: "",
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
if (!s.registrationUrl) {
|
|
788
|
-
return derive({ ...s, actionMessage: copyParam("event.registrationOnPage", "Registration for this event happens on the event page.") });
|
|
789
|
-
}
|
|
1230
|
+
if (s.registrationKind !== "external" || !s.registrationUrl) return derive(s);
|
|
790
1231
|
try {
|
|
791
1232
|
const result = await open(s.registrationUrl, "in_app_browser");
|
|
792
|
-
return derive({ ...s,
|
|
1233
|
+
return derive({ ...s, snackbarMessage: result.opened ? "" : copyParam("event.registrationOpenFailed", "Could not open the registration page.") });
|
|
793
1234
|
} catch {
|
|
794
|
-
return derive({ ...s,
|
|
1235
|
+
return derive({ ...s, snackbarMessage: copyParam("event.registrationOpenFailed", "Could not open the registration page.") });
|
|
795
1236
|
}
|
|
796
1237
|
},
|
|
797
1238
|
share: async (s) => {
|
|
@@ -801,11 +1242,11 @@ const svc = defineService<S, {
|
|
|
801
1242
|
await shareSheet({ subject: s.title, text, ...(s.shareUrl ? { url: s.shareUrl } : {}) });
|
|
802
1243
|
return s;
|
|
803
1244
|
} catch {
|
|
804
|
-
return derive({ ...s,
|
|
1245
|
+
return derive({ ...s, snackbarMessage: copyParam("event.shareUnavailable", "Sharing is not available on this device.") });
|
|
805
1246
|
}
|
|
806
1247
|
},
|
|
807
1248
|
addToCalendar: async (s) => {
|
|
808
|
-
if (!s.calendarStart) return derive({ ...s,
|
|
1249
|
+
if (!s.calendarStart) return derive({ ...s, rsvpActionsOpen: false, snackbarMessage: copyParam("event.noDateMessage", "This event does not have a date yet.") });
|
|
809
1250
|
const end = s.calendarEnd > s.calendarStart ? s.calendarEnd : s.calendarStart + 60 * 60 * 1000;
|
|
810
1251
|
try {
|
|
811
1252
|
let permission = await getPermissionStatus();
|
|
@@ -814,7 +1255,7 @@ const svc = defineService<S, {
|
|
|
814
1255
|
}
|
|
815
1256
|
if (permission !== "granted" && permission !== "limited") {
|
|
816
1257
|
const fallback = await open(calendarFallbackUrl(s, end), "in_app_browser");
|
|
817
|
-
return derive({ ...s,
|
|
1258
|
+
return derive({ ...s, rsvpActionsOpen: false, snackbarMessage: fallback.opened ? copyParam("event.calendarOpened", "Opened calendar.") : copyParam("event.calendarPermissionDenied", "Calendar permission was not granted.") });
|
|
818
1259
|
}
|
|
819
1260
|
const created = await createEvent({
|
|
820
1261
|
title: s.title || "Event",
|
|
@@ -825,25 +1266,26 @@ const svc = defineService<S, {
|
|
|
825
1266
|
});
|
|
826
1267
|
if (!created.id) {
|
|
827
1268
|
const fallback = await open(calendarFallbackUrl(s, end), "in_app_browser");
|
|
828
|
-
return derive({ ...s,
|
|
1269
|
+
return derive({ ...s, rsvpActionsOpen: false, snackbarMessage: fallback.opened ? copyParam("event.calendarOpened", "Opened calendar.") : copyParam("event.calendarAddFailed", "Could not add this event to calendar."), isAddingCalendar: false });
|
|
829
1270
|
}
|
|
830
1271
|
return derive({
|
|
831
1272
|
...s,
|
|
832
|
-
|
|
1273
|
+
rsvpActionsOpen: false,
|
|
1274
|
+
snackbarMessage: copyParam("event.calendarAdded", "Added to calendar."),
|
|
833
1275
|
isAddingCalendar: false,
|
|
834
1276
|
});
|
|
835
1277
|
} catch {
|
|
836
|
-
return derive({ ...s,
|
|
1278
|
+
return derive({ ...s, rsvpActionsOpen: false, snackbarMessage: copyParam("event.calendarAddFailed", "Could not add this event to calendar."), isAddingCalendar: false });
|
|
837
1279
|
}
|
|
838
1280
|
},
|
|
839
1281
|
// Only http(s), mailto and tel links reach open_url; a refused link, or one nothing opened, says so, and a
|
|
840
1282
|
// link that opens clears an earlier message.
|
|
841
1283
|
openLink: async (s, arg) => derive({
|
|
842
1284
|
...s,
|
|
843
|
-
|
|
1285
|
+
snackbarMessage: (await openRichLink(arg)) ? "" : copyParam("event.linkOpenFailed", "This link could not be opened."),
|
|
844
1286
|
}),
|
|
845
1287
|
},
|
|
846
1288
|
});
|
|
847
|
-
resetUnhydratedKeysOnOlderRuntime(svc, { ...svc.getState(), ...ATTENDANCE_INITIAL, ticketPurchaseBlocked: true }, [...ATTENDANCE_NOT_HYDRATED, "ticketPurchaseBlocked"]);
|
|
1289
|
+
resetUnhydratedKeysOnOlderRuntime(svc, { ...svc.getState(), ...ATTENDANCE_INITIAL, ticketPurchaseBlocked: true, ticketSheetOpen: false, rsvpActionsOpen: false, cancelConfirmOpen: false, rsvpId: "", rsvpState: "none", rsvpLabel: "" }, [...ATTENDANCE_NOT_HYDRATED, "ticketPurchaseBlocked", "ticketSheetOpen", "rsvpActionsOpen", "cancelConfirmOpen", "rsvpId", "rsvpState", "rsvpLabel"]);
|
|
848
1290
|
svc.load();
|
|
849
1291
|
export { svc };
|