@wix/himalaya-cli 0.1495.0 → 0.1497.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/sdk/iap/src/attendance-classifier.d.ts +37 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/catalog-pricing-contract.d.ts +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/index.d.ts +2 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/core/sdk/iap/src/wix-payments-contract.d.ts +11 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/apple-iap-contract.d.ts +27 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/commerce-contract.d.ts +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/iap-verticals/attendance.d.ts +10 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/iap-verticals/index.d.ts +1 -0
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/flows/iap-checkout/src/index.d.ts +1 -1
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/events/src/index.d.ts +33 -2
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap-checkout.mjs +346 -26
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/iap.mjs +132 -3
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/screens.mjs +366 -48
- package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-events.mjs +5 -0
- package/dist/cli.mjs +816 -667
- package/guides/iap-products.md +7 -1
- package/guides/sdk-reference.json +376 -27
- package/package.json +2 -2
- package/templates/beauty-salon/dev/index.ts +394 -0
- package/templates/beauty-salon/tier5-src/_shared/iap-attendance-form.ts +106 -0
- package/templates/beauty-salon/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
- package/templates/beauty-salon/tier5-src/_shared/iap-attendance.ts +38 -0
- package/templates/beauty-salon/tier5-src/himi-booking-time/index.ts +52 -7
- package/templates/branded-lite/dev/index.ts +1073 -284
- package/templates/branded-lite/tier5-src/_shared/business-chat-model.ts +35 -4
- package/templates/branded-lite/tier5-src/_shared/business-chat-transport.ts +12 -5
- package/templates/branded-lite/tier5-src/_shared/iap-attendance-form.ts +106 -0
- package/templates/branded-lite/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
- package/templates/branded-lite/tier5-src/_shared/iap-attendance.ts +38 -0
- package/templates/branded-lite/tier5-src/business-chat/index.ts +50 -17
- package/templates/branded-lite/tier5-src/himi-booking-time/index.ts +52 -7
- package/templates/branded-lite/tier5-src/himi-events-detail/index.ts +112 -21
- package/templates/community-club/dev/index.ts +397 -2
- package/templates/community-club/tier5-src/_shared/iap-attendance-form.ts +106 -0
- package/templates/community-club/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
- package/templates/community-club/tier5-src/_shared/iap-attendance.ts +38 -0
- package/templates/community-club/tier5-src/himi-events-detail/index.ts +112 -21
- package/templates/events-meetups/dev/index.ts +397 -2
- package/templates/events-meetups/tier5-src/_shared/iap-attendance-form.ts +106 -0
- package/templates/events-meetups/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
- package/templates/events-meetups/tier5-src/_shared/iap-attendance.ts +38 -0
- package/templates/events-meetups/tier5-src/himi-events-detail/index.ts +112 -21
- package/templates/gym-fitness/dev/index.ts +828 -39
- package/templates/gym-fitness/tier5-src/_shared/iap-attendance-form.ts +106 -0
- package/templates/gym-fitness/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
- package/templates/gym-fitness/tier5-src/_shared/iap-attendance.ts +38 -0
- package/templates/gym-fitness/tier5-src/himi-booking-time/index.ts +52 -7
- package/templates/gym-fitness/tier5-src/himi-events-detail/index.ts +112 -21
- package/templates/nonprofit-cause/dev/index.ts +397 -2
- package/templates/nonprofit-cause/tier5-src/_shared/iap-attendance-form.ts +106 -0
- package/templates/nonprofit-cause/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
- package/templates/nonprofit-cause/tier5-src/_shared/iap-attendance.ts +38 -0
- package/templates/nonprofit-cause/tier5-src/himi-events-detail/index.ts +112 -21
- package/templates/podcast-creator/dev/index.ts +397 -2
- package/templates/podcast-creator/tier5-src/_shared/iap-attendance-form.ts +106 -0
- package/templates/podcast-creator/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
- package/templates/podcast-creator/tier5-src/_shared/iap-attendance.ts +38 -0
- package/templates/podcast-creator/tier5-src/himi-events-detail/index.ts +112 -21
- package/templates/portfolio-creative/dev/index.ts +397 -2
- package/templates/portfolio-creative/tier5-src/_shared/iap-attendance-form.ts +106 -0
- package/templates/portfolio-creative/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
- package/templates/portfolio-creative/tier5-src/_shared/iap-attendance.ts +38 -0
- package/templates/portfolio-creative/tier5-src/himi-events-detail/index.ts +112 -21
- package/templates/restaurant/dev/index.ts +397 -2
- package/templates/restaurant/tier5-src/_shared/iap-attendance-form.ts +106 -0
- package/templates/restaurant/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
- package/templates/restaurant/tier5-src/_shared/iap-attendance.ts +38 -0
- package/templates/restaurant/tier5-src/himi-events-detail/index.ts +112 -21
- package/templates/yoga-studio/dev/index.ts +828 -39
- package/templates/yoga-studio/tier5-src/_shared/iap-attendance-form.ts +106 -0
- package/templates/yoga-studio/tier5-src/_shared/iap-attendance-purchase.ts +211 -0
- package/templates/yoga-studio/tier5-src/_shared/iap-attendance.ts +38 -0
- package/templates/yoga-studio/tier5-src/himi-booking-time/index.ts +52 -7
- package/templates/yoga-studio/tier5-src/himi-events-detail/index.ts +112 -21
|
@@ -19,6 +19,9 @@ import { isBridgeUnavailable } from "../_shared/owner-load-error.js";
|
|
|
19
19
|
import { emitHostEvent, hostEventInstant } from "../_shared/host-events.js";
|
|
20
20
|
import { availabilityBeyond, slotIdAt } from "../_shared/himi-bookings.js";
|
|
21
21
|
import { wixMediaUrl, WIX_DETAIL_IMAGE } from "@himalaya/wix-media";
|
|
22
|
+
import { mayUseRegularBookingCheckout } from "../_shared/iap-attendance.js";
|
|
23
|
+
import { ATTENDANCE_INITIAL, ATTENDANCE_NOT_HYDRATED, createAttendancePurchase, type AttendancePurchaseFields } from "../_shared/iap-attendance-purchase.js";
|
|
24
|
+
import { resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
|
|
22
25
|
|
|
23
26
|
// Offline-first (data-offline): booking a slot is WRITE-BEHIND against the
|
|
24
27
|
// `member_bookings` entity (the same rows the My Bookings worker reads). The
|
|
@@ -53,7 +56,7 @@ interface SlotRow {
|
|
|
53
56
|
notSelected: boolean;
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
interface S {
|
|
59
|
+
interface S extends AttendancePurchaseFields {
|
|
57
60
|
status: "loading" | "ready";
|
|
58
61
|
id: string;
|
|
59
62
|
serviceName: string;
|
|
@@ -132,6 +135,10 @@ interface DayOption {
|
|
|
132
135
|
}
|
|
133
136
|
|
|
134
137
|
let slotsById = new Map<string, WixBookingSlotFull>();
|
|
138
|
+
let regularCheckoutAllowed = false;
|
|
139
|
+
let regularServicePrice = "";
|
|
140
|
+
let lastReady: S | undefined;
|
|
141
|
+
const applePurchase = createAttendancePurchase({ kind: "bookingsService", prefix: "time", pollAction: "himiBookingTime.iapPoll", onChange: () => { void svc.iapPublish(); } });
|
|
135
142
|
/** Every slot row in the window, across all days. Kept out of worker state so
|
|
136
143
|
* the state the host receives holds one day's rows, not the whole window. */
|
|
137
144
|
let allRows: SlotRow[] = [];
|
|
@@ -326,18 +333,24 @@ function withDay(s: S, day: string): S {
|
|
|
326
333
|
}
|
|
327
334
|
|
|
328
335
|
function derive(s: S): S {
|
|
336
|
+
const regular = regularCheckoutAllowed || applePurchase.usesRegularPlatform();
|
|
329
337
|
// `hasSlots` is the WHOLE window (it drives the screen-level empty state); the
|
|
330
338
|
// selected day having none is a per-day line, not an empty screen.
|
|
331
339
|
const hasSlots = s.days.some((d) => d.hasSlots) || s.slots.length > 0;
|
|
332
340
|
const hasDaySlots = s.slots.length > 0;
|
|
333
341
|
const hasSelected = !!s.selectedSlotId;
|
|
334
342
|
const hasSuccess = !!s.successMessage;
|
|
335
|
-
const canBook = s.isMember && hasSelected && !s.bookingInFlight && !hasSuccess;
|
|
336
|
-
const showChoosePrompt = s.isMember && hasSlots && !hasSelected && !s.bookingInFlight && !hasSuccess;
|
|
343
|
+
const canBook = regular && s.isMember && hasSelected && !s.bookingInFlight && !hasSuccess;
|
|
344
|
+
const showChoosePrompt = regular && s.isMember && hasSlots && !hasSelected && !s.bookingInFlight && !hasSuccess;
|
|
337
345
|
const isRegistering = !!s.isRegistering;
|
|
338
|
-
|
|
346
|
+
const next: S = {
|
|
339
347
|
...s,
|
|
348
|
+
_poll: undefined,
|
|
349
|
+
servicePrice: regular ? regularServicePrice : "",
|
|
350
|
+
serviceLine: s.status === "ready" ? [s.serviceDuration, regular ? regularServicePrice : "", copyParam("time.availabilityWindow", "Available in the next 14 days")].filter(Boolean).join(" · ") : s.serviceLine,
|
|
351
|
+
message: regular && !regularCheckoutAllowed && s.message === copyParam("time.purchaseUnavailable", "Purchasing is temporarily unavailable.") ? "" : s.message,
|
|
340
352
|
hasSlots,
|
|
353
|
+
...applePurchase.fields(hasSelected),
|
|
341
354
|
hasSelected,
|
|
342
355
|
noSelected: !hasSelected,
|
|
343
356
|
hideSignIn: s.isMember || !hasSelected,
|
|
@@ -375,6 +388,8 @@ function derive(s: S): S {
|
|
|
375
388
|
? copyParam("time.authBodyRegister", "Create a member account, then book the selected time on this screen.")
|
|
376
389
|
: copyParam("time.authBody", "You can browse times as a visitor. Booking is saved to a member account."),
|
|
377
390
|
};
|
|
391
|
+
if (next.status === "ready") lastReady = next;
|
|
392
|
+
return next;
|
|
378
393
|
}
|
|
379
394
|
|
|
380
395
|
async function probeMember(s: S): Promise<S> {
|
|
@@ -499,8 +514,15 @@ const svc = defineService<S, {
|
|
|
499
514
|
signIn: (s: S) => Promise<S>;
|
|
500
515
|
register: (s: S) => Promise<S>;
|
|
501
516
|
bookSelected: (s: S) => Promise<S>;
|
|
517
|
+
buyApple: (s: S) => Promise<S>;
|
|
518
|
+
setIapField: (s: S, arg?: unknown) => S;
|
|
519
|
+
iapPublish: (s: S) => S;
|
|
520
|
+
iapRefresh: (s: S) => Promise<S>;
|
|
521
|
+
iapPoll: (s: S) => Promise<S>;
|
|
502
522
|
}>("himiBookingTime", {
|
|
523
|
+
notHydrated: [...ATTENDANCE_NOT_HYDRATED, "email", "password", "firstName", "lastName", "isMember", "canBook", "cannotBook"],
|
|
503
524
|
state: derive({
|
|
525
|
+
...ATTENDANCE_INITIAL,
|
|
504
526
|
status: "loading",
|
|
505
527
|
id,
|
|
506
528
|
serviceName: "Choose a time",
|
|
@@ -555,6 +577,16 @@ const svc = defineService<S, {
|
|
|
555
577
|
bookingsScreenId: "himi_bookings",
|
|
556
578
|
}),
|
|
557
579
|
actions: {
|
|
580
|
+
iapPublish: (s) => derive(lastReady ?? s),
|
|
581
|
+
iapRefresh: async (): Promise<S> => { await applePurchase.refresh(); return derive(svc.getState()); },
|
|
582
|
+
iapPoll: async (): Promise<S> => { await applePurchase.poll(); return derive(svc.getState()); },
|
|
583
|
+
setIapField: (s, arg) => { applePurchase.change(arg); return derive(s); },
|
|
584
|
+
buyApple: async (s: S): Promise<S> => {
|
|
585
|
+
const slot = slotsById.get(s.selectedSlotId);
|
|
586
|
+
if (!slot || !s.isMember) return derive(s);
|
|
587
|
+
await applePurchase.buy({ key: s.selectedSlotId, slot: slot as unknown as Record<string, unknown> });
|
|
588
|
+
return derive(svc.getState());
|
|
589
|
+
},
|
|
558
590
|
load: async (s, arg): Promise<S> => {
|
|
559
591
|
const revisionAtStart = selectionRevision;
|
|
560
592
|
stashCopy(arg);
|
|
@@ -598,6 +630,10 @@ const svc = defineService<S, {
|
|
|
598
630
|
}
|
|
599
631
|
allRows = rows;
|
|
600
632
|
const days = daysOf(rows);
|
|
633
|
+
const header = serviceHeaderFields(serviceRes.service);
|
|
634
|
+
regularServicePrice = header.price;
|
|
635
|
+
regularCheckoutAllowed = await mayUseRegularBookingCheckout(serviceRes.service);
|
|
636
|
+
await applePurchase.booking(s.id, serviceRes.service);
|
|
601
637
|
// A reload runs in the background when the screen shows again. A time the customer
|
|
602
638
|
// picked while it ran wins over the selection this load started from.
|
|
603
639
|
const chosen: S = selectionRevision !== revisionAtStart ? (svc.getState?.() ?? next) : next;
|
|
@@ -617,7 +653,6 @@ const svc = defineService<S, {
|
|
|
617
653
|
// otherwise open on the first day that has any.
|
|
618
654
|
const keep = days.find((d) => d.id === chosen.selectedDay && d.hasSlots);
|
|
619
655
|
const initialDay = startRow?.dayKey ?? startDay?.id ?? picked?.dayKey ?? keep?.id ?? days.find((d) => d.hasSlots)?.id ?? days[0]?.id ?? "";
|
|
620
|
-
const header = serviceHeaderFields(serviceRes.service);
|
|
621
656
|
return derive(withDay({
|
|
622
657
|
...next,
|
|
623
658
|
days,
|
|
@@ -628,13 +663,14 @@ const svc = defineService<S, {
|
|
|
628
663
|
status: "ready",
|
|
629
664
|
errored: false,
|
|
630
665
|
serviceName: serviceRes.service?.name || copyParam("time.serviceNameFallback", "Choose a time"),
|
|
631
|
-
serviceLine: [header.duration, header.price, copyParam("time.availabilityWindow", "Available in the next 14 days")].filter(Boolean).join(" · "),
|
|
666
|
+
serviceLine: [header.duration, regularCheckoutAllowed ? header.price : "", copyParam("time.availabilityWindow", "Available in the next 14 days")].filter(Boolean).join(" · "),
|
|
632
667
|
serviceImage: header.image,
|
|
633
668
|
hasServiceImage: !!header.image,
|
|
634
669
|
noServiceImage: !header.image,
|
|
635
|
-
servicePrice: header.price,
|
|
670
|
+
servicePrice: regularCheckoutAllowed ? header.price : "",
|
|
636
671
|
serviceDuration: header.duration,
|
|
637
672
|
serviceIsPaid: header.paid,
|
|
673
|
+
message: regularCheckoutAllowed || !applePurchase.fields(true, false).iapBuyHidden ? "" : copyParam("time.purchaseUnavailable", "Purchasing is temporarily unavailable."),
|
|
638
674
|
}, initialDay));
|
|
639
675
|
} catch {
|
|
640
676
|
slotsById = new Map();
|
|
@@ -727,6 +763,14 @@ const svc = defineService<S, {
|
|
|
727
763
|
if (!s.selectedSlotId) return derive({ ...s, message: copyParam("time.bookNeedsSlot", "Choose one time first.") });
|
|
728
764
|
const slot = slotsById.get(s.selectedSlotId);
|
|
729
765
|
if (!slot) return derive({ ...s, message: copyParam("time.bookSlotGone", "That time is no longer available. Try again.") });
|
|
766
|
+
try {
|
|
767
|
+
const current = await getService(s.id);
|
|
768
|
+
regularCheckoutAllowed = await mayUseRegularBookingCheckout(current.service);
|
|
769
|
+
if (serviceHeaderFields(current.service).paid !== s.serviceIsPaid) return derive({ ...s, message: copyParam("time.purchaseUnavailable", "Purchasing is temporarily unavailable.") });
|
|
770
|
+
} catch {
|
|
771
|
+
if (s.serviceIsPaid) return derive({ ...s, message: copyParam("time.purchaseUnavailable", "Purchasing is temporarily unavailable.") });
|
|
772
|
+
}
|
|
773
|
+
if (!regularCheckoutAllowed) return derive({ ...s, message: copyParam("time.purchaseUnavailable", "Purchasing is temporarily unavailable.") });
|
|
730
774
|
// A confirmed booking is reported to the app hosting this screen (an embed's `onAction`), so a
|
|
731
775
|
// host chat can say so and refresh what it shows. Dropped where nothing hosts the screen.
|
|
732
776
|
// The booking's own instants carry an offset; a slot's may be LOCAL to its timezone, which
|
|
@@ -867,5 +911,6 @@ const svc = defineService<S, {
|
|
|
867
911
|
},
|
|
868
912
|
},
|
|
869
913
|
});
|
|
914
|
+
resetUnhydratedKeysOnOlderRuntime(svc, { ...svc.getState(), ...ATTENDANCE_INITIAL, email: "", password: "", firstName: "", lastName: "", isMember: false, canBook: false, cannotBook: true }, [...ATTENDANCE_NOT_HYDRATED, "email", "password", "firstName", "lastName", "isMember", "canBook", "cannotBook"]);
|
|
870
915
|
svc.load();
|
|
871
916
|
export { svc };
|
|
@@ -36,6 +36,23 @@ import {
|
|
|
36
36
|
} from "../_shared/himi-events";
|
|
37
37
|
import { initials } from "../_shared/himi-text.js";
|
|
38
38
|
import { hostedSitePageOpener, isHostedWorker } from "../_shared/hosted-checkout.js";
|
|
39
|
+
import { mayUseRegularTicketCheckout } from "../_shared/iap-attendance.js";
|
|
40
|
+
import { ATTENDANCE_INITIAL, ATTENDANCE_NOT_HYDRATED, createAttendancePurchase, type AttendancePurchaseFields } from "../_shared/iap-attendance-purchase.js";
|
|
41
|
+
import { resetUnhydratedKeysOnOlderRuntime } from "../_shared/iap-worker.js";
|
|
42
|
+
|
|
43
|
+
let ticketCheckoutAllowed = false;
|
|
44
|
+
let paymentEvent: unknown;
|
|
45
|
+
let paymentTickets = new Map<string, WixAvailableTicket>();
|
|
46
|
+
let ticketSelectionVersion = 0;
|
|
47
|
+
let lastReady: S | undefined;
|
|
48
|
+
const applePurchase = createAttendancePurchase({ kind: "eventsTicket", prefix: "event", pollAction: "himiEventDetail.iapPoll", onChange: () => { void svc.iapPublish(); } });
|
|
49
|
+
|
|
50
|
+
async function priceTicket(event: unknown, id: string, version: number): Promise<boolean> {
|
|
51
|
+
const available = paymentTickets.get(id);
|
|
52
|
+
if (version !== ticketSelectionVersion) return false;
|
|
53
|
+
await applePurchase.ticket(id, event, available);
|
|
54
|
+
return version === ticketSelectionVersion;
|
|
55
|
+
}
|
|
39
56
|
|
|
40
57
|
/** A hosted signed-in event-page open is in flight (see openRegistration). */
|
|
41
58
|
let hostedOpenInFlight = false;
|
|
@@ -88,7 +105,7 @@ async function writeCachedRow(row: EventRow): Promise<void> {
|
|
|
88
105
|
try { await upsert(ENTITY, row as unknown as DbEntity); } catch { /* capability not granted → online-only */ }
|
|
89
106
|
}
|
|
90
107
|
|
|
91
|
-
interface S {
|
|
108
|
+
interface S extends AttendancePurchaseFields {
|
|
92
109
|
status: "loading" | "ready";
|
|
93
110
|
id: string;
|
|
94
111
|
title: string;
|
|
@@ -139,6 +156,8 @@ interface S {
|
|
|
139
156
|
/** The tier the CTA will reserve. Defaults to the cheapest on-sale tier so the
|
|
140
157
|
* button is never dead, and follows the buyer's tap after that. */
|
|
141
158
|
selectedTierId: string;
|
|
159
|
+
ticketPurchaseBlocked: boolean;
|
|
160
|
+
regularTicketsHidden: boolean;
|
|
142
161
|
/** "General · $25" — echoed next to the CTA so the choice is visible. */
|
|
143
162
|
selectedTierLabel: string;
|
|
144
163
|
/** External/ticketed registration URL when the payload carries one. */
|
|
@@ -179,7 +198,7 @@ function tierPrice(t: WixAvailableTicket): string {
|
|
|
179
198
|
}
|
|
180
199
|
|
|
181
200
|
function tierAvailable(t: WixAvailableTicket): boolean {
|
|
182
|
-
return !!t.id && !/SALE_ENDED|CANCELED|SOLD_OUT/i.test(t.saleStatus ?? "");
|
|
201
|
+
return !!t.id && !/SALE_SCHEDULED|SALE_ENDED|CANCELED|SOLD_OUT/i.test(t.saleStatus ?? "");
|
|
183
202
|
}
|
|
184
203
|
|
|
185
204
|
/** Map the buyer-visible tiers onto rows, cheapest first (so the default pick is
|
|
@@ -205,26 +224,36 @@ function tierRows(defs: WixAvailableTicket[], selectedId: string): TicketTierRow
|
|
|
205
224
|
}
|
|
206
225
|
|
|
207
226
|
/** Best-effort tier read — never sinks the screen if the tier endpoint fails. */
|
|
208
|
-
async function loadTiers(eventId: string): Promise<TicketTierRow[]> {
|
|
227
|
+
async function loadTiers(eventId: string): Promise<{ rows: TicketTierRow[]; tickets: Map<string, WixAvailableTicket> }> {
|
|
209
228
|
try {
|
|
210
229
|
const response = await queryAvailableTickets(eventId);
|
|
211
230
|
const defs = response.definitions ?? [];
|
|
231
|
+
const tickets = new Map(defs.filter((t) => t.id).map((t) => [t.id!, t]));
|
|
212
232
|
const cheapestAvailable = defs
|
|
213
233
|
.filter(tierAvailable)
|
|
214
234
|
.slice()
|
|
215
235
|
.sort((a, b) => Number(a.price?.amount ?? 0) - Number(b.price?.amount ?? 0))[0];
|
|
216
|
-
return tierRows(defs, cheapestAvailable?.id ?? "");
|
|
236
|
+
return { rows: tierRows(defs, cheapestAvailable?.id ?? ""), tickets };
|
|
217
237
|
} catch {
|
|
218
|
-
return [];
|
|
238
|
+
return { rows: [], tickets: new Map() };
|
|
219
239
|
}
|
|
220
240
|
}
|
|
221
241
|
|
|
222
242
|
function derive(s: S): S {
|
|
243
|
+
const regular = ticketCheckoutAllowed || applePurchase.usesRegularPlatform();
|
|
223
244
|
const hasContent = !!s.title;
|
|
224
245
|
const hasMapMarkers = s.mapMarkers.length > 0;
|
|
225
246
|
const hasAttendees = s.attendeeAvatars.length > 0;
|
|
226
|
-
|
|
247
|
+
const purchase = applePurchase.fields(!!s.selectedTierId);
|
|
248
|
+
const tiers = s.ticketTiers.map((t) => ({ ...t, priceLine: regular ? tierPrice(paymentTickets.get(t.id) ?? {}) : t.id === s.selectedTierId ? purchase.iapPrice : "" }));
|
|
249
|
+
const next = {
|
|
227
250
|
...s,
|
|
251
|
+
_poll: undefined,
|
|
252
|
+
...purchase,
|
|
253
|
+
actionMessage: regular && !ticketCheckoutAllowed && s.actionMessage === copyParam("event.purchaseUnavailable", "Purchasing is temporarily unavailable.") ? "" : s.actionMessage,
|
|
254
|
+
ticketTiers: tiers,
|
|
255
|
+
regularTicketsHidden: s.registrationKind !== "tickets" || !purchase.iapBuyHidden,
|
|
256
|
+
ticketPurchaseBlocked: s.registrationKind === "tickets" && !regular,
|
|
228
257
|
hasDescriptionJson: s.descriptionJson !== "",
|
|
229
258
|
noMap: !s.mapUrl,
|
|
230
259
|
hasMapMarkers,
|
|
@@ -239,8 +268,8 @@ function derive(s: S): S {
|
|
|
239
268
|
showTierPicker: s.ticketTiers.length > 1,
|
|
240
269
|
hideTierPicker: s.ticketTiers.length <= 1,
|
|
241
270
|
selectedTierLabel: (() => {
|
|
242
|
-
const picked =
|
|
243
|
-
return picked ?
|
|
271
|
+
const picked = tiers.find((t) => t.id === s.selectedTierId);
|
|
272
|
+
return picked ? [picked.name, picked.priceLine].filter(Boolean).join(" · ") : "";
|
|
244
273
|
})(),
|
|
245
274
|
showTickets: s.registrationKind === "tickets",
|
|
246
275
|
hideTickets: s.registrationKind !== "tickets",
|
|
@@ -256,6 +285,8 @@ function derive(s: S): S {
|
|
|
256
285
|
? copyParam("event.errorText", "Event could not be loaded.")
|
|
257
286
|
: copyParam("event.notFoundText", "Event not found.")),
|
|
258
287
|
};
|
|
288
|
+
if (next.status === "ready") lastReady = next;
|
|
289
|
+
return next;
|
|
259
290
|
}
|
|
260
291
|
|
|
261
292
|
function calendarStamp(ms: number): string {
|
|
@@ -288,7 +319,7 @@ function calendarFallbackUrl(s: S, end: number): string {
|
|
|
288
319
|
|
|
289
320
|
/** The Events v3 fieldsets the detail reads (see `WixEventFieldset`). DETAILS carries mainImage and
|
|
290
321
|
* shortDescription: REGISTRATION fills eventType, which skips the query enrichment that used to supply them. */
|
|
291
|
-
export const EVENT_DETAIL_FIELDSETS: WixEventFieldset[] = ["DETAILS", "TEXTS", "REGISTRATION", "URLS"];
|
|
322
|
+
export const EVENT_DETAIL_FIELDSETS: WixEventFieldset[] = ["DETAILS", "TEXTS", "REGISTRATION", "URLS", "FORM"];
|
|
292
323
|
|
|
293
324
|
/** Map the decoded registration/eventType onto the CTA variant. The decoded
|
|
294
325
|
* `eventType` already folds registration.type / registration.initialType /
|
|
@@ -401,16 +432,23 @@ async function enrichRegistration(id: string, e: WixEvent): Promise<WixEvent> {
|
|
|
401
432
|
}
|
|
402
433
|
|
|
403
434
|
const svc = defineService<S, {
|
|
404
|
-
load: (s: S, arg?: unknown) => Promise<S>;
|
|
435
|
+
load: (s: S, arg?: unknown) => Promise<S | undefined>;
|
|
405
436
|
openMap: (s: S) => Promise<S>;
|
|
406
|
-
selectTier: (s: S, arg?: unknown) => Promise<S>;
|
|
437
|
+
selectTier: (s: S, arg?: unknown) => Promise<S | undefined>;
|
|
407
438
|
openRegistration: (s: S) => Promise<S>;
|
|
408
439
|
share: (s: S) => Promise<S>;
|
|
409
440
|
addToCalendar: (s: S) => Promise<S>;
|
|
410
441
|
/** [rico] #3372 — RichContent's on_link: `{ url }` of the tapped link. */
|
|
411
442
|
openLink: (s: S, arg?: unknown) => Promise<S>;
|
|
443
|
+
buyApple: (s: S) => Promise<S>;
|
|
444
|
+
setIapField: (s: S, arg?: unknown) => S;
|
|
445
|
+
iapPublish: (s: S) => S;
|
|
446
|
+
iapRefresh: (s: S) => Promise<S>;
|
|
447
|
+
iapPoll: (s: S) => Promise<S>;
|
|
412
448
|
}>("himiEventDetail", {
|
|
449
|
+
notHydrated: [...ATTENDANCE_NOT_HYDRATED, "ticketPurchaseBlocked"],
|
|
413
450
|
state: derive({
|
|
451
|
+
...ATTENDANCE_INITIAL,
|
|
414
452
|
status: "loading",
|
|
415
453
|
id: inputId(),
|
|
416
454
|
title: "",
|
|
@@ -433,6 +471,8 @@ const svc = defineService<S, {
|
|
|
433
471
|
hasAttendees: false,
|
|
434
472
|
noAttendees: true,
|
|
435
473
|
registrationKind: "rsvp",
|
|
474
|
+
ticketPurchaseBlocked: true,
|
|
475
|
+
regularTicketsHidden: true,
|
|
436
476
|
showRsvp: true,
|
|
437
477
|
hideRsvp: false,
|
|
438
478
|
showTickets: false,
|
|
@@ -462,7 +502,19 @@ const svc = defineService<S, {
|
|
|
462
502
|
noEmpty: true,
|
|
463
503
|
}),
|
|
464
504
|
actions: {
|
|
465
|
-
|
|
505
|
+
iapPublish: (s) => derive(lastReady ?? s),
|
|
506
|
+
iapRefresh: async (): Promise<S> => { await applePurchase.refresh(); return derive(svc.getState()); },
|
|
507
|
+
iapPoll: async (): Promise<S> => { await applePurchase.poll(); return derive(svc.getState()); },
|
|
508
|
+
setIapField: (s, arg) => { applePurchase.change(arg); return derive(s); },
|
|
509
|
+
buyApple: async (s: S): Promise<S> => {
|
|
510
|
+
if (!s.selectedTierId || !s.ticketTiers.some((t) => t.id === s.selectedTierId && t.available)) return derive(s);
|
|
511
|
+
await applePurchase.buy({ key: s.selectedTierId, eventId: s.id });
|
|
512
|
+
return derive(svc.getState());
|
|
513
|
+
},
|
|
514
|
+
load: async (s: S, arg?: unknown): Promise<S | undefined> => {
|
|
515
|
+
const selectionVersion = ++ticketSelectionVersion;
|
|
516
|
+
ticketCheckoutAllowed = false;
|
|
517
|
+
applePurchase.invalidate();
|
|
466
518
|
stashCopy(arg);
|
|
467
519
|
const id = s.id || inputId();
|
|
468
520
|
if (!id) return derive({ ...s, status: "ready", errored: true });
|
|
@@ -479,7 +531,15 @@ const svc = defineService<S, {
|
|
|
479
531
|
// screen BEFORE the CTA is tapped. Best-effort: a tier read that fails
|
|
480
532
|
// leaves the picker collapsed and the CTA falls back to its own read.
|
|
481
533
|
const kind = registrationKindOf(e);
|
|
482
|
-
const
|
|
534
|
+
const loaded = kind === "tickets" ? await loadTiers(id) : { rows: [], tickets: new Map<string, WixAvailableTicket>() };
|
|
535
|
+
const tiers = loaded.rows;
|
|
536
|
+
const selectedTierId = tiers.find((t) => t.isSelected)?.id ?? tiers.find((t) => t.available)?.id ?? "";
|
|
537
|
+
const regular = kind !== "tickets" || await mayUseRegularTicketCheckout(e, loaded.tickets.get(selectedTierId));
|
|
538
|
+
if (selectionVersion !== ticketSelectionVersion) return undefined;
|
|
539
|
+
paymentEvent = e;
|
|
540
|
+
paymentTickets = loaded.tickets;
|
|
541
|
+
ticketCheckoutAllowed = regular;
|
|
542
|
+
if (kind === "tickets" && !await priceTicket(e, selectedTierId, selectionVersion)) return undefined;
|
|
483
543
|
// Write-through: cache the fetched row (same EventRow shape as the list) so a
|
|
484
544
|
// later offline open of this detail can fall back to it.
|
|
485
545
|
await writeCachedRow(normalizeEvent(e));
|
|
@@ -493,7 +553,7 @@ const svc = defineService<S, {
|
|
|
493
553
|
ticketTiers: tiers,
|
|
494
554
|
// Default the pick to the cheapest AVAILABLE tier so the CTA is never
|
|
495
555
|
// dead; the buyer's tap overrides it from there.
|
|
496
|
-
selectedTierId
|
|
556
|
+
selectedTierId,
|
|
497
557
|
title,
|
|
498
558
|
initial: (title.trim()[0] || "E").toUpperCase(),
|
|
499
559
|
when: eventWhen(e),
|
|
@@ -514,11 +574,12 @@ const svc = defineService<S, {
|
|
|
514
574
|
shareUrl: eventPageUrlOf(e),
|
|
515
575
|
calendarStart: eventStartMillis(e) ?? 0,
|
|
516
576
|
calendarEnd: eventEndMillis(e) ?? 0,
|
|
517
|
-
actionMessage: "",
|
|
577
|
+
actionMessage: ticketCheckoutAllowed || !applePurchase.fields(true, false).iapBuyHidden ? "" : copyParam("event.purchaseUnavailable", "Purchasing is temporarily unavailable."),
|
|
518
578
|
statusLabel: titleCase(e.eventStatus) || titleCase((e as unknown as { status?: string }).status),
|
|
519
579
|
});
|
|
520
580
|
} catch {
|
|
521
581
|
// Offline / fetch failure: fall back to the last-synced cached row before erroring.
|
|
582
|
+
if (selectionVersion !== ticketSelectionVersion) return undefined;
|
|
522
583
|
const cached = await readCachedRow(id);
|
|
523
584
|
if (cached) {
|
|
524
585
|
const title = cached.title || "Event";
|
|
@@ -552,22 +613,46 @@ const svc = defineService<S, {
|
|
|
552
613
|
return derive({ ...s, actionMessage: copyParam("event.mapOpenFailed", "Could not open maps on this device.") });
|
|
553
614
|
}
|
|
554
615
|
},
|
|
555
|
-
selectTier: async (s, arg) => {
|
|
616
|
+
selectTier: async (s: S, arg?: unknown): Promise<S | undefined> => {
|
|
556
617
|
const id = typeof arg === "string" ? arg : (arg as { id?: unknown } | undefined)?.id;
|
|
557
618
|
const wanted = typeof id === "string" ? id : "";
|
|
558
619
|
const tier = s.ticketTiers.find((t) => t.id === wanted);
|
|
559
620
|
// Ignore a tap on an off-sale row rather than letting the CTA promise a
|
|
560
621
|
// ticket it cannot reserve.
|
|
561
622
|
if (!tier || !tier.available) return s;
|
|
623
|
+
const selectionVersion = ++ticketSelectionVersion;
|
|
624
|
+
ticketCheckoutAllowed = false;
|
|
625
|
+
applePurchase.invalidate();
|
|
626
|
+
const regular = await mayUseRegularTicketCheckout(paymentEvent, paymentTickets.get(wanted));
|
|
627
|
+
if (selectionVersion !== ticketSelectionVersion) return undefined;
|
|
628
|
+
ticketCheckoutAllowed = regular;
|
|
629
|
+
if (!await priceTicket(paymentEvent, wanted, selectionVersion)) return undefined;
|
|
562
630
|
return derive({
|
|
563
631
|
...s,
|
|
564
632
|
selectedTierId: wanted,
|
|
565
633
|
ticketTiers: s.ticketTiers.map((t) => ({ ...t, isSelected: t.id === wanted, notSelected: t.id !== wanted })),
|
|
566
634
|
// A new pick invalidates whatever the last attempt said.
|
|
567
|
-
actionMessage: "",
|
|
635
|
+
actionMessage: ticketCheckoutAllowed || !applePurchase.fields(true, false).iapBuyHidden ? "" : copyParam("event.purchaseUnavailable", "Purchasing is temporarily unavailable."),
|
|
568
636
|
});
|
|
569
637
|
},
|
|
570
|
-
openRegistration: async (s) => {
|
|
638
|
+
openRegistration: async (s): Promise<S> => {
|
|
639
|
+
let eventPageAllowed = true;
|
|
640
|
+
let checkoutEvent: WixEvent | undefined;
|
|
641
|
+
if (s.registrationKind === "tickets") {
|
|
642
|
+
try {
|
|
643
|
+
const current = await getEvent(s.id, { fields: EVENT_DETAIL_FIELDSETS });
|
|
644
|
+
const event = ((current as unknown as { event?: WixEvent }).event ?? current) as WixEvent;
|
|
645
|
+
checkoutEvent = event;
|
|
646
|
+
eventPageAllowed = await mayUseRegularTicketCheckout(event, undefined);
|
|
647
|
+
ticketCheckoutAllowed = eventPageAllowed;
|
|
648
|
+
if (!ticketCheckoutAllowed) {
|
|
649
|
+
const tickets = await queryAvailableTickets(s.id);
|
|
650
|
+
const selected = (tickets.definitions ?? []).find((ticket) => ticket.id === s.selectedTierId);
|
|
651
|
+
ticketCheckoutAllowed = await mayUseRegularTicketCheckout(event, selected);
|
|
652
|
+
}
|
|
653
|
+
if (!ticketCheckoutAllowed) return derive({ ...s, actionMessage: copyParam("event.purchaseUnavailable", "Purchasing is temporarily unavailable.") });
|
|
654
|
+
} catch { return derive({ ...s, actionMessage: copyParam("event.purchaseUnavailable", "Purchasing is temporarily unavailable.") }); }
|
|
655
|
+
}
|
|
571
656
|
// Phase 2c — NATIVE ticket purchase. Reserve the cheapest available tier,
|
|
572
657
|
// turn the hold into an order, then settle it on the shared in-app pay rail
|
|
573
658
|
// (@wix/himalaya/checkout): wallet where the app has one, in-app Wix cashier
|
|
@@ -598,9 +683,12 @@ const svc = defineService<S, {
|
|
|
598
683
|
// picker is collapsed.
|
|
599
684
|
const tickets = await queryAvailableTickets(s.id);
|
|
600
685
|
const live = (tickets.definitions ?? []).filter(tierAvailable);
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
686
|
+
// A recovered first read must show the offer before it can be sold.
|
|
687
|
+
if (live.length > 0 && !s.selectedTierId && s.ticketTiers.length === 0) return svc.load();
|
|
688
|
+
const tier = live.find((t) => t.id === s.selectedTierId);
|
|
689
|
+
if (!tier) return derive({ ...s, actionMessage: copyParam("event.ticketChanged", "That ticket is no longer available. Choose a ticket again.") });
|
|
690
|
+
ticketCheckoutAllowed = await mayUseRegularTicketCheckout(checkoutEvent, tier);
|
|
691
|
+
if (!ticketCheckoutAllowed) return derive({ ...s, actionMessage: copyParam("event.purchaseUnavailable", "Purchasing is temporarily unavailable.") });
|
|
604
692
|
if (tier?.id) {
|
|
605
693
|
const held = await createTicketReservation(s.id, [{ ticketDefinitionId: tier.id, quantity: 1 }]);
|
|
606
694
|
reservationId = held.id ?? "";
|
|
@@ -643,6 +731,7 @@ const svc = defineService<S, {
|
|
|
643
731
|
// fall through to the hosted rail below
|
|
644
732
|
}
|
|
645
733
|
// Hosted eventsCheckout — pinned to the live reservation when we have one.
|
|
734
|
+
if (!eventPageAllowed && !reservationId) return derive({ ...s, actionMessage: copyParam("event.purchaseUnavailable", "Purchasing is temporarily unavailable.") });
|
|
646
735
|
try {
|
|
647
736
|
const params = reservationId ? { reservationId } : s.eventSlug ? { eventSlug: s.eventSlug } : undefined;
|
|
648
737
|
if (params) {
|
|
@@ -667,6 +756,7 @@ const svc = defineService<S, {
|
|
|
667
756
|
// page → nothing) on its own. Nothing here can change between its prepare and its open (the event
|
|
668
757
|
// is fixed and the page is where the member chooses and pays), so the guard has nothing to re-read.
|
|
669
758
|
// A host that refused or failed leaves the visitor page below.
|
|
759
|
+
if (!eventPageAllowed) return derive({ ...s, actionMessage: copyParam("event.purchaseUnavailable", "Purchasing is temporarily unavailable.") });
|
|
670
760
|
if (isHostedWorker() && s.id && (s.registrationKind === "tickets" || s.registrationKind === "rsvp")) {
|
|
671
761
|
// One signed-in open at a time: the host keeps a single one-time ticket, so a second tap would
|
|
672
762
|
// replace the first tap's ticket and drop that tap into the signed-OUT visitor page below.
|
|
@@ -747,5 +837,6 @@ const svc = defineService<S, {
|
|
|
747
837
|
}),
|
|
748
838
|
},
|
|
749
839
|
});
|
|
840
|
+
resetUnhydratedKeysOnOlderRuntime(svc, { ...svc.getState(), ...ATTENDANCE_INITIAL, ticketPurchaseBlocked: true }, [...ATTENDANCE_NOT_HYDRATED, "ticketPurchaseBlocked"]);
|
|
750
841
|
svc.load();
|
|
751
842
|
export { svc };
|