@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
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { attendanceCheckoutRoute, availableTicketAttendanceOf, bookingAttendanceOf, classifyForApple, hostSessionFromGlobal, iapPriceFields, type BookingAttendance, type CatalogReference, type IapPollDirective, type TicketAttendance } from "@himalaya/iap";
|
|
2
|
+
import { bookingOrders, createIapCommerce, eventTicketOrders, type IapOrderItem } from "@himalaya/iap-checkout";
|
|
3
|
+
import { copyParam, copyTemplate } from "./himi-copy.js";
|
|
4
|
+
import { createAttendanceForm, type AttendanceField } from "./iap-attendance-form.js";
|
|
5
|
+
import { attendancePlatform } from "./iap-attendance.js";
|
|
6
|
+
import { iapPriceCopy, iapPurchaseCopy } from "./iap-worker.js";
|
|
7
|
+
|
|
8
|
+
export interface AttendancePurchaseFields {
|
|
9
|
+
iapBuyHidden: boolean;
|
|
10
|
+
iapBuyDisabled: boolean;
|
|
11
|
+
iapBuyLabel: string;
|
|
12
|
+
iapBusy: boolean;
|
|
13
|
+
iapPrice: string;
|
|
14
|
+
iapNote: string;
|
|
15
|
+
iapRetryHidden: boolean;
|
|
16
|
+
iapFormHidden: boolean;
|
|
17
|
+
iapFormError: string;
|
|
18
|
+
iapSignInHidden: boolean;
|
|
19
|
+
iapFields: AttendanceField[];
|
|
20
|
+
_poll?: IapPollDirective;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const ATTENDANCE_INITIAL: AttendancePurchaseFields = {
|
|
24
|
+
iapBuyHidden: true, iapBuyDisabled: true, iapBuyLabel: "", iapBusy: false,
|
|
25
|
+
iapPrice: "", iapNote: "", iapRetryHidden: true, iapFormHidden: true, iapFormError: "", iapSignInHidden: true, iapFields: [],
|
|
26
|
+
};
|
|
27
|
+
export const ATTENDANCE_NOT_HYDRATED = [...Object.keys(ATTENDANCE_INITIAL), "_poll"] as (keyof AttendancePurchaseFields & string)[];
|
|
28
|
+
|
|
29
|
+
export function createAttendancePurchase(options: { kind: "bookingsService" | "eventsTicket"; prefix: string; pollAction: string; onChange(): void }) {
|
|
30
|
+
const { prefix } = options;
|
|
31
|
+
let form = createAttendanceForm(prefix);
|
|
32
|
+
const submittedForms = new Map<string, ReturnType<typeof createAttendanceForm>>();
|
|
33
|
+
const session = hostSessionFromGlobal();
|
|
34
|
+
const commerce = createIapCommerce({ adapters: [options.kind === "bookingsService" ? bookingOrders : eventTicketOrders], onChange: options.onChange });
|
|
35
|
+
let generation = session.current().generation;
|
|
36
|
+
let reference: CatalogReference | undefined;
|
|
37
|
+
let metadata: BookingAttendance | TicketAttendance | undefined;
|
|
38
|
+
let platform = "unknown";
|
|
39
|
+
let started = false;
|
|
40
|
+
let inFlight = false;
|
|
41
|
+
let formError = "";
|
|
42
|
+
let sequence = 0;
|
|
43
|
+
let formRevision = 0;
|
|
44
|
+
const attempts = new Map<string, string>();
|
|
45
|
+
session.subscribe(() => { syncSession(); if (reference) options.onChange(); });
|
|
46
|
+
|
|
47
|
+
function syncSession(): void {
|
|
48
|
+
const next = session.current().generation;
|
|
49
|
+
if (next === generation) return;
|
|
50
|
+
generation = next;
|
|
51
|
+
form.clear();
|
|
52
|
+
submittedForms.clear();
|
|
53
|
+
formError = "";
|
|
54
|
+
attempts.clear();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function refresh(): Promise<void> {
|
|
58
|
+
const next = await attendancePlatform(publishPlatform);
|
|
59
|
+
if (next !== "unknown" || platform === "unknown") platform = next;
|
|
60
|
+
await resume();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function resume(): Promise<void> {
|
|
64
|
+
if (reference) await commerce.loadPrices([{ reference }]);
|
|
65
|
+
const first = !started;
|
|
66
|
+
started = true;
|
|
67
|
+
await (first ? commerce.start() : commerce.reconcile());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function publishPlatform(next: string): void {
|
|
71
|
+
if (next === "unknown" && platform !== "unknown") return;
|
|
72
|
+
platform = next;
|
|
73
|
+
void resume().then(options.onChange, options.onChange);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function show(): void {
|
|
77
|
+
void attendancePlatform(publishPlatform).then(publishPlatform, options.onChange);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function booking(id: string, service: unknown): Promise<void> {
|
|
81
|
+
syncSession();
|
|
82
|
+
reference = { kind: options.kind, id };
|
|
83
|
+
metadata = bookingAttendanceOf(service);
|
|
84
|
+
selectForm(id, (next) => next.booking(service));
|
|
85
|
+
formRevision++;
|
|
86
|
+
formError = "";
|
|
87
|
+
await show();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function ticket(id: string, event: unknown, available: unknown): Promise<void> {
|
|
91
|
+
syncSession();
|
|
92
|
+
reference = id ? { kind: options.kind, id } : undefined;
|
|
93
|
+
metadata = availableTicketAttendanceOf(event, available);
|
|
94
|
+
selectForm(id, (next) => next.event((event as { form?: unknown } | undefined)?.form));
|
|
95
|
+
formRevision++;
|
|
96
|
+
formError = "";
|
|
97
|
+
await show();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function selectForm(id: string, initialize: (next: ReturnType<typeof createAttendanceForm>) => void): void {
|
|
101
|
+
const key = `${options.kind}:${id}`;
|
|
102
|
+
const retained = submittedForms.get(key);
|
|
103
|
+
form = retained ?? createAttendanceForm(prefix);
|
|
104
|
+
if (!retained) initialize(form);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function fields(selected = true, publishPoll = true): AttendancePurchaseFields {
|
|
108
|
+
syncSession();
|
|
109
|
+
if (!reference || !metadata) return ATTENDANCE_INITIAL;
|
|
110
|
+
const currency = metadata.currency ?? "";
|
|
111
|
+
const classified = classifyForApple(metadata, currency);
|
|
112
|
+
const candidate = platform === "ios" && classified === "appleIap";
|
|
113
|
+
const price = iapPriceFields(commerce.pricing.view(reference), iapPriceCopy(prefix));
|
|
114
|
+
const copy = { ...iapPurchaseCopy(prefix), buy: copyParam(`${prefix}.appleBuyCta`, options.kind === "bookingsService" ? "Book with Apple" : "Buy ticket with Apple"), buyAnother: copyParam(`${prefix}.buyAnotherCta`, options.kind === "bookingsService" ? "Book again" : "Buy another ticket") };
|
|
115
|
+
const purchase = commerce.purchaseFields(reference, copy);
|
|
116
|
+
const apple = candidate || (platform === "ios" && classified !== "wixPayments" && purchase.appleRoute);
|
|
117
|
+
const enabled = attendanceCheckoutRoute(metadata, currency, platform) === "appleIap";
|
|
118
|
+
const poll = publishPoll ? commerce.pollDirective(options.pollAction) : undefined;
|
|
119
|
+
const state = commerce.state();
|
|
120
|
+
for (const [target, attempt] of attempts) {
|
|
121
|
+
const key = `${reference.kind}:${reference.id}#${encodeURIComponent(attempt)}`;
|
|
122
|
+
if ((state.phase === "accessReady" && state.item === key) || state.payments.some((p) => p.item === key && p.access === "granted")) attempts.delete(target);
|
|
123
|
+
}
|
|
124
|
+
const formKey = `${reference.kind}:${reference.id}`;
|
|
125
|
+
if (![...attempts.keys()].some((target) => target.startsWith(`${formKey}:`))) submittedForms.delete(formKey);
|
|
126
|
+
const own = state.item?.startsWith(`${reference.kind}:${reference.id}#`);
|
|
127
|
+
const completed = state.phase === "accessReady" && own;
|
|
128
|
+
const member = session.current().kind === "member";
|
|
129
|
+
let note = "";
|
|
130
|
+
if (apple && !enabled) note = copy.unavailable;
|
|
131
|
+
else if (apple && !member) note = copy.signInRequired;
|
|
132
|
+
else if (apple && purchase.buyNote) note = purchase.buyNote;
|
|
133
|
+
else if (apple && !form.supported()) note = copyParam(`${prefix}.formUnavailableText`, "This registration form is not supported yet.");
|
|
134
|
+
else if (apple && completed) note = copyParam(`${prefix}.purchaseCompleteText`, options.kind === "bookingsService" ? "Your booking is confirmed." : "Your ticket is confirmed.");
|
|
135
|
+
else if (apple && own && state.phase === "cancelled") note = copyParam(`${prefix}.purchaseCancelledText`, "Purchase cancelled. Your place is not confirmed.");
|
|
136
|
+
return {
|
|
137
|
+
iapBuyHidden: !apple, iapBuyDisabled: !enabled || !selected || !member || !form.supported() || purchase.buyDisabled || inFlight,
|
|
138
|
+
iapBuyLabel: purchase.buyLabel || copy.buy, iapBusy: purchase.buyBusy || inFlight,
|
|
139
|
+
iapPrice: apple ? price.priceText : "", iapRetryHidden: !apple || (price.hidePriceRetry && commerce.scopeComplete()),
|
|
140
|
+
iapNote: note,
|
|
141
|
+
iapFormHidden: !apple || !selected || !form.supported(), iapFormError: formError,
|
|
142
|
+
iapSignInHidden: !apple || member || options.kind === "bookingsService",
|
|
143
|
+
iapFields: form.rows().map((f, index, rows) => ({ ...f,
|
|
144
|
+
id: `${generation}:${formRevision}:${f.id}`, fieldId: f.id,
|
|
145
|
+
errorHidden: !f.error,
|
|
146
|
+
options: f.options.map((o) => ({ ...o, fieldId: f.id, displayLabel: (f.kind === "multiple" ? f.selectedIds.includes(o.id) : f.value === o.id) ? `${o.label} ✓` : o.label })),
|
|
147
|
+
label: f.required ? `${f.label} *` : f.label,
|
|
148
|
+
inputLabel: copyTemplate(`${prefix}.fieldAccessibilityLabel`, "{section}: {field}", { section: f.id.startsWith("buyer:") ? copyParam(`${prefix}.buyerDetailsLabel`, "Buyer details") : copyParam(`${prefix}.guestDetailsLabel`, "Ticket holder details"), field: f.label }),
|
|
149
|
+
sectionLabel: index === 0 ? copyParam(`${prefix}.buyerDetailsLabel`, "Buyer details") : f.id.startsWith("guest:") && rows[index - 1].id.startsWith("buyer:") ? copyParam(`${prefix}.guestDetailsLabel`, "Ticket holder details") : "",
|
|
150
|
+
})), ...(poll ? { _poll: poll } : {}),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function buy(selection: { key: string; slot?: Record<string, unknown>; eventId?: string }): Promise<void> {
|
|
155
|
+
syncSession();
|
|
156
|
+
if (!reference || !metadata || fields(true, false).iapBuyDisabled || inFlight) return;
|
|
157
|
+
if (options.kind === "eventsTicket" && reference.id !== selection.key) return;
|
|
158
|
+
if (!form.validate()) { formError = copyParam(`${prefix}.formRequiredText`, "Complete the required details before purchasing."); return; }
|
|
159
|
+
if (session.current().kind !== "member") { formError = iapPurchaseCopy(prefix).signInRequired; return; }
|
|
160
|
+
const currentGeneration = generation;
|
|
161
|
+
const ref = reference;
|
|
162
|
+
submittedForms.set(`${ref.kind}:${ref.id}`, form);
|
|
163
|
+
const target = `${ref.kind}:${ref.id}:${selection.key}`;
|
|
164
|
+
let attemptId = attempts.get(target);
|
|
165
|
+
const state = commerce.state();
|
|
166
|
+
const attemptKey = attemptId ? `${ref.kind}:${ref.id}#${encodeURIComponent(attemptId)}` : "";
|
|
167
|
+
const completed = attemptId && ((state.phase === "accessReady" && state.item === attemptKey) || state.payments.some((p) => p.item === attemptKey && p.access === "granted"));
|
|
168
|
+
if (!attemptId || completed) { attemptId = `${Date.now().toString(36)}-${++sequence}-${Math.random().toString(36).slice(2)}`; attempts.set(target, attemptId); }
|
|
169
|
+
const item: IapOrderItem = { reference: ref, attemptId, quantity: 1, attendance: {
|
|
170
|
+
amount: metadata.amount!, currency: metadata.currency!,
|
|
171
|
+
...(options.kind === "bookingsService" ? { booking: { slot: selection.slot ?? {}, contactDetails: form.buyer() } } : { ticket: { eventId: selection.eventId ?? "", ...form.ticket(ref.id) } }),
|
|
172
|
+
} };
|
|
173
|
+
inFlight = true;
|
|
174
|
+
formError = "";
|
|
175
|
+
options.onChange();
|
|
176
|
+
try {
|
|
177
|
+
await commerce.buy(item);
|
|
178
|
+
const after = commerce.state();
|
|
179
|
+
if (after.phase === "accessReady" && after.item === `${ref.kind}:${ref.id}#${encodeURIComponent(attemptId)}`) attempts.delete(target);
|
|
180
|
+
}
|
|
181
|
+
finally { inFlight = false; syncSession(); if (generation !== currentGeneration) form.clear(); options.onChange(); }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function change(arg: unknown): void {
|
|
185
|
+
syncSession();
|
|
186
|
+
const state = commerce.state();
|
|
187
|
+
if (reference) {
|
|
188
|
+
for (const [target, attempt] of attempts) {
|
|
189
|
+
const item = `${reference.kind}:${reference.id}#${encodeURIComponent(attempt)}`;
|
|
190
|
+
const canCorrect = state.phase === "cancelled" || (state.phase === "refused" && (state.failure === "orderRefused" || state.failure === "productChanged"));
|
|
191
|
+
if (canCorrect && state.item === item) { attempts.delete(target); submittedForms.delete(`${reference.kind}:${reference.id}`); }
|
|
192
|
+
}
|
|
193
|
+
const referencePrefix = `${reference.kind}:${reference.id}:`;
|
|
194
|
+
if (inFlight || [...attempts.keys()].some((target) => target.startsWith(referencePrefix))) {
|
|
195
|
+
formError = copyParam(`${prefix}.detailsLockedText`, "Finish the current purchase before changing registration details.");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
form.change(arg);
|
|
200
|
+
formError = "";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
booking, ticket, fields, buy,
|
|
205
|
+
usesRegularPlatform: () => platform === "android" || platform === "web",
|
|
206
|
+
invalidate: () => { reference = undefined; metadata = undefined; options.onChange(); },
|
|
207
|
+
change,
|
|
208
|
+
refresh,
|
|
209
|
+
poll: async () => { syncSession(); await commerce.poll(); },
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { get as appInfo } from "@himalaya/app-info";
|
|
2
|
+
import { attendanceCheckoutRoute, bookingAttendanceOf, ticketAttendanceOf } from "@himalaya/iap";
|
|
3
|
+
|
|
4
|
+
let knownPlatform: string | undefined;
|
|
5
|
+
|
|
6
|
+
export function attendancePlatform(onResolved?: (platform: string) => void): Promise<string> {
|
|
7
|
+
if (knownPlatform) return Promise.resolve(knownPlatform);
|
|
8
|
+
const info = appInfo().then((value) => value.platform, () => "unknown").then((platform) => {
|
|
9
|
+
if (platform !== "unknown") knownPlatform = platform;
|
|
10
|
+
return platform;
|
|
11
|
+
});
|
|
12
|
+
const host = globalThis as { setTimeout?: (fn: () => void, ms: number) => unknown; clearTimeout?: (timer: unknown) => void };
|
|
13
|
+
if (!host.setTimeout) {
|
|
14
|
+
void info.then((platform) => onResolved?.(platform));
|
|
15
|
+
return Promise.resolve("unknown");
|
|
16
|
+
}
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
let timedOut = false;
|
|
19
|
+
const timer = host.setTimeout!(() => { timedOut = true; resolve("unknown"); }, 1500);
|
|
20
|
+
void info.then((platform) => { host.clearTimeout?.(timer); resolve(platform); if (timedOut) onResolved?.(platform); });
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function mayUseRegularBookingCheckout(service: unknown): Promise<boolean> {
|
|
25
|
+
const metadata = bookingAttendanceOf(service);
|
|
26
|
+
if (metadata.priceType === "NO_FEE" || isZero(metadata.amount)) return true;
|
|
27
|
+
return attendanceCheckoutRoute(metadata, metadata.currency ?? "", await attendancePlatform()) === "regular";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function mayUseRegularTicketCheckout(event: unknown, ticket: { free?: boolean; price?: { amount?: string; currency?: string }; saleStatus?: string } | undefined): Promise<boolean> {
|
|
31
|
+
if (ticket?.free === true || isZero(ticket?.price?.amount)) return true;
|
|
32
|
+
const metadata = ticketAttendanceOf(event, { hidden: false, saleStatus: ticket?.saleStatus });
|
|
33
|
+
return attendanceCheckoutRoute(metadata, ticket?.price?.currency ?? "", await attendancePlatform()) === "regular";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isZero(value: unknown): boolean {
|
|
37
|
+
return typeof value === "string" && /^0+(?:\.0+)?$/.test(value);
|
|
38
|
+
}
|
|
@@ -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 };
|