@ticketboothapp/booking 1.2.101 → 1.2.102
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/package.json +1 -1
- package/src/components/booking/BookingDialog.module.css +9 -0
- package/src/components/booking/BookingProductGrid.module.css +11 -0
- package/src/components/booking/BookingProductGrid.tsx +54 -28
- package/src/components/booking/CancellationPolicySelector.tsx +4 -1
- package/src/components/booking/CheckoutForm.module.css +108 -3
- package/src/components/booking/CheckoutForm.tsx +13 -1
- package/src/components/booking/CheckoutOptionalPhoneFields.tsx +58 -0
- package/src/components/booking/DapTourDescription.tsx +9 -7
- package/src/components/booking/DependentAddOnBookingDialog.tsx +42 -7
- package/src/components/booking/NewBookingFlow.tsx +137 -55
- package/src/components/booking/PrivateShuttleBookingFlow.module.css +7 -0
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +21 -0
- package/src/components/booking/booking-flow-types.ts +2 -0
- package/src/components/booking/booking-flow-ui.ts +2 -0
- package/src/components/booking/booking-flow.css +72 -4
- package/src/data/dap-descriptions/session-couples-families-friends.en.json +0 -3
- package/src/data/dap-descriptions/session-elopements.en.json +12 -12
- package/src/data/dap-descriptions/session-proposals.en.json +6 -9
- package/src/data/products-config.json +20 -0
- package/src/lib/booking/checkout-contact.ts +8 -0
- package/src/lib/booking/i18n/messages/en.json +6 -0
- package/src/lib/booking/i18n/messages/fr.json +6 -0
- package/src/lib/booking/phone.ts +18 -0
- package/src/lib/booking-api.ts +131 -2
- package/src/lib/booking-types.ts +5 -0
- package/src/lib/dap-descriptions.ts +6 -0
- package/src/lib/dependent-add-on-api.ts +6 -0
- package/src/lib/photo-dap-config.ts +92 -15
- package/src/providers/dependent-add-on-dialog-provider.tsx +6 -0
- package/src/runtime/types.ts +2 -0
- package/src/strings/en.json +6 -6
|
@@ -72,6 +72,7 @@ import { useBookingHost } from '../../runtime';
|
|
|
72
72
|
import type { BookingFlowUiOptions } from './booking-flow-ui';
|
|
73
73
|
import type { NewBookingFlowProps } from './booking-flow-types';
|
|
74
74
|
import { BOOKING_FLOW_ABANDON_EVENT } from '../../providers/booking-dialog-provider';
|
|
75
|
+
import { optionalCheckoutContactFields } from '../../lib/booking/checkout-contact';
|
|
75
76
|
|
|
76
77
|
function formatTicketLineItemsForSummary(lines: Array<{ category: string; qty: number }>): string {
|
|
77
78
|
const labels: Record<string, string> = {
|
|
@@ -176,6 +177,74 @@ function availabilityWithOptionId(availability: Availability, fallbackOptionId?:
|
|
|
176
177
|
return productOptionId ? { ...availability, productOptionId } : availability;
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
/** Prefer full availability rows over calendar summary rows when merging fetches. */
|
|
181
|
+
function mergeAvailabilityRecord(
|
|
182
|
+
existing: Availability | undefined,
|
|
183
|
+
incoming: Availability,
|
|
184
|
+
): Availability {
|
|
185
|
+
if (!existing) return incoming;
|
|
186
|
+
const existingIsSummary = existing.isSummary === true;
|
|
187
|
+
const incomingIsSummary = incoming.isSummary === true;
|
|
188
|
+
if (existingIsSummary && !incomingIsSummary) return incoming;
|
|
189
|
+
if (!existingIsSummary && incomingIsSummary) return existing;
|
|
190
|
+
return incoming;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function mergeAvailabilitiesIntoMap(
|
|
194
|
+
map: Map<string, Availability>,
|
|
195
|
+
availabilities: Availability[],
|
|
196
|
+
): void {
|
|
197
|
+
for (const availability of availabilities) {
|
|
198
|
+
const key = `${availability.dateTime}-${availability.productOptionId}`;
|
|
199
|
+
map.set(key, mergeAvailabilityRecord(map.get(key), availability));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function pickDefaultAvailabilityForTimes(
|
|
204
|
+
times: Availability[],
|
|
205
|
+
activeOptions: Product['options'],
|
|
206
|
+
): Availability | null {
|
|
207
|
+
if (times.length === 0) return null;
|
|
208
|
+
const mostPopularOption = activeOptions.find((opt) => opt.mostPopular);
|
|
209
|
+
const candidate = mostPopularOption
|
|
210
|
+
? times.find(
|
|
211
|
+
(avail) => avail.productOptionId === mostPopularOption.optionId && avail.vacancies > 0,
|
|
212
|
+
)
|
|
213
|
+
: null;
|
|
214
|
+
const fallback = times.find((avail) => avail.vacancies > 0);
|
|
215
|
+
return candidate ?? fallback ?? null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function availabilityMatchesSelectedDate(
|
|
219
|
+
availability: Availability,
|
|
220
|
+
selectedDate: string,
|
|
221
|
+
companyTimezone: string,
|
|
222
|
+
): boolean {
|
|
223
|
+
try {
|
|
224
|
+
const dateInCompanyTz = formatInTimeZone(
|
|
225
|
+
parseAvailabilityDateTime(availability.dateTime),
|
|
226
|
+
companyTimezone,
|
|
227
|
+
'yyyy-MM-dd',
|
|
228
|
+
);
|
|
229
|
+
return dateInCompanyTz === selectedDate;
|
|
230
|
+
} catch {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function shouldSyncSelectedAvailability(
|
|
236
|
+
current: Availability,
|
|
237
|
+
updated: Availability,
|
|
238
|
+
): boolean {
|
|
239
|
+
return (
|
|
240
|
+
current !== updated &&
|
|
241
|
+
(current.isSummary !== updated.isSummary ||
|
|
242
|
+
current.vacancies !== updated.vacancies ||
|
|
243
|
+
current.availabilityId !== updated.availabilityId ||
|
|
244
|
+
(current.returnOptions?.length ?? 0) !== (updated.returnOptions?.length ?? 0))
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
179
248
|
function mergePrecomputedPricesFromAvailabilityResult(
|
|
180
249
|
next: Record<string, PrecomputedPricesByCategory>,
|
|
181
250
|
result: GetAvailabilitiesResponse,
|
|
@@ -343,6 +412,7 @@ export function NewBookingFlow({
|
|
|
343
412
|
hideItineraryBox = false,
|
|
344
413
|
flowUi,
|
|
345
414
|
afterItinerary,
|
|
415
|
+
beforeCancellationPolicy,
|
|
346
416
|
augmentItineraryItems,
|
|
347
417
|
augmentPriceSummary,
|
|
348
418
|
dependentAddOnSelection,
|
|
@@ -376,6 +446,7 @@ export function NewBookingFlow({
|
|
|
376
446
|
const [email, setEmail] = useState('');
|
|
377
447
|
const [firstName, setFirstName] = useState('');
|
|
378
448
|
const [lastName, setLastName] = useState('');
|
|
449
|
+
const [phoneNumber, setPhoneNumber] = useState('');
|
|
379
450
|
const [promoCodeInput, setPromoCodeInput] = useState('');
|
|
380
451
|
const [appliedPromoCode, setAppliedPromoCode] = useState<string | null>(null);
|
|
381
452
|
const [promoCodeError, setPromoCodeError] = useState('');
|
|
@@ -474,7 +545,6 @@ export function NewBookingFlow({
|
|
|
474
545
|
const hasHydratedAddOnsFromReceiptRef = useRef(false);
|
|
475
546
|
const hasAutoSelectedPartnerDateRef = useRef(false);
|
|
476
547
|
const hasAutoSelectedPartnerPickupRef = useRef(false);
|
|
477
|
-
const handleDateSelectRef = useRef<(date: string) => void>(() => {});
|
|
478
548
|
useEffect(() => {
|
|
479
549
|
setPartnerAttributionConfirmed(false);
|
|
480
550
|
}, [flowUi?.partnerAttributionSummary, flowUi?.partnerAttributionConfirmLabel]);
|
|
@@ -632,9 +702,7 @@ export function NewBookingFlow({
|
|
|
632
702
|
const existingMap = new Map(
|
|
633
703
|
prev.map((avail) => [`${avail.dateTime}-${avail.productOptionId}`, avail])
|
|
634
704
|
);
|
|
635
|
-
allFetchedAvailabilities
|
|
636
|
-
existingMap.set(`${avail.dateTime}-${avail.productOptionId}`, avail);
|
|
637
|
-
});
|
|
705
|
+
mergeAvailabilitiesIntoMap(existingMap, allFetchedAvailabilities);
|
|
638
706
|
mergedOut = Array.from(existingMap.values());
|
|
639
707
|
return mergedOut;
|
|
640
708
|
});
|
|
@@ -668,9 +736,7 @@ export function NewBookingFlow({
|
|
|
668
736
|
const mergedAvailabilitiesMap = new Map(
|
|
669
737
|
existingAvailabilities.map((a) => [`${a.dateTime}-${a.productOptionId}`, a])
|
|
670
738
|
);
|
|
671
|
-
allFetchedAvailabilities
|
|
672
|
-
mergedAvailabilitiesMap.set(`${a.dateTime}-${a.productOptionId}`, a);
|
|
673
|
-
});
|
|
739
|
+
mergeAvailabilitiesIntoMap(mergedAvailabilitiesMap, allFetchedAvailabilities);
|
|
674
740
|
const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
|
|
675
741
|
results.forEach((r) => {
|
|
676
742
|
mergePrecomputedPricesFromAvailabilityResult(mergedPrecomputed, r);
|
|
@@ -753,9 +819,7 @@ export function NewBookingFlow({
|
|
|
753
819
|
let mergedOut: Availability[] = [];
|
|
754
820
|
setAvailabilities((prev) => {
|
|
755
821
|
const merged = new Map(prev.map((availability) => [`${availability.dateTime}-${availability.productOptionId}`, availability]));
|
|
756
|
-
detailedAvailabilities
|
|
757
|
-
merged.set(`${availability.dateTime}-${availability.productOptionId}`, availability);
|
|
758
|
-
});
|
|
822
|
+
mergeAvailabilitiesIntoMap(merged, detailedAvailabilities);
|
|
759
823
|
mergedOut = Array.from(merged.values());
|
|
760
824
|
return mergedOut;
|
|
761
825
|
});
|
|
@@ -776,9 +840,7 @@ export function NewBookingFlow({
|
|
|
776
840
|
availability,
|
|
777
841
|
])
|
|
778
842
|
);
|
|
779
|
-
detailedAvailabilities
|
|
780
|
-
mergedAvailabilities.set(`${availability.dateTime}-${availability.productOptionId}`, availability);
|
|
781
|
-
});
|
|
843
|
+
mergeAvailabilitiesIntoMap(mergedAvailabilities, detailedAvailabilities);
|
|
782
844
|
const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
|
|
783
845
|
results.forEach((r) => mergePrecomputedPricesFromAvailabilityResult(mergedPrecomputed, r));
|
|
784
846
|
availabilitiesCache.merge(cacheKey, {
|
|
@@ -1013,12 +1075,7 @@ export function NewBookingFlow({
|
|
|
1013
1075
|
);
|
|
1014
1076
|
|
|
1015
1077
|
// Merge new availabilities - update existing ones or add new ones
|
|
1016
|
-
|
|
1017
|
-
const key = `${avail.dateTime}-${avail.productOptionId}`;
|
|
1018
|
-
// Always update to get latest data (vacancies, prices, etc.)
|
|
1019
|
-
existingMap.set(key, avail);
|
|
1020
|
-
});
|
|
1021
|
-
|
|
1078
|
+
mergeAvailabilitiesIntoMap(existingMap, allFetchedAvailabilities);
|
|
1022
1079
|
return Array.from(existingMap.values());
|
|
1023
1080
|
});
|
|
1024
1081
|
|
|
@@ -1045,9 +1102,7 @@ export function NewBookingFlow({
|
|
|
1045
1102
|
const mergedAvailabilitiesMap = new Map(
|
|
1046
1103
|
existingAvailabilities.map((a) => [`${a.dateTime}-${a.productOptionId}`, a])
|
|
1047
1104
|
);
|
|
1048
|
-
allFetchedAvailabilities
|
|
1049
|
-
mergedAvailabilitiesMap.set(`${a.dateTime}-${a.productOptionId}`, a);
|
|
1050
|
-
});
|
|
1105
|
+
mergeAvailabilitiesIntoMap(mergedAvailabilitiesMap, allFetchedAvailabilities);
|
|
1051
1106
|
const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
|
|
1052
1107
|
results.forEach((r) => {
|
|
1053
1108
|
mergePrecomputedPricesFromAvailabilityResult(mergedPrecomputed, r);
|
|
@@ -1216,9 +1271,7 @@ export function NewBookingFlow({
|
|
|
1216
1271
|
|
|
1217
1272
|
setAvailabilities((prev) => {
|
|
1218
1273
|
const merged = new Map(prev.map((availability) => [`${availability.dateTime}-${availability.productOptionId}`, availability]));
|
|
1219
|
-
detailedAvailabilities
|
|
1220
|
-
merged.set(`${availability.dateTime}-${availability.productOptionId}`, availability);
|
|
1221
|
-
});
|
|
1274
|
+
mergeAvailabilitiesIntoMap(merged, detailedAvailabilities);
|
|
1222
1275
|
return Array.from(merged.values());
|
|
1223
1276
|
});
|
|
1224
1277
|
|
|
@@ -1238,9 +1291,7 @@ export function NewBookingFlow({
|
|
|
1238
1291
|
availability,
|
|
1239
1292
|
])
|
|
1240
1293
|
);
|
|
1241
|
-
detailedAvailabilities
|
|
1242
|
-
mergedAvailabilities.set(`${availability.dateTime}-${availability.productOptionId}`, availability);
|
|
1243
|
-
});
|
|
1294
|
+
mergeAvailabilitiesIntoMap(mergedAvailabilities, detailedAvailabilities);
|
|
1244
1295
|
const mergedPrecomputed = { ...(existingCache?.precomputedPricesByOption ?? {}) };
|
|
1245
1296
|
results.forEach((result) => {
|
|
1246
1297
|
mergePrecomputedPricesFromAvailabilityResult(mergedPrecomputed, result);
|
|
@@ -1423,6 +1474,11 @@ export function NewBookingFlow({
|
|
|
1423
1474
|
[timesForSelectedDate],
|
|
1424
1475
|
);
|
|
1425
1476
|
|
|
1477
|
+
const selectedAvailabilityMatchesDate = useMemo(() => {
|
|
1478
|
+
if (!selectedAvailability || !selectedDate) return false;
|
|
1479
|
+
return availabilityMatchesSelectedDate(selectedAvailability, selectedDate, companyTimezone);
|
|
1480
|
+
}, [selectedAvailability, selectedDate, companyTimezone]);
|
|
1481
|
+
|
|
1426
1482
|
useEffect(() => {
|
|
1427
1483
|
if (hasAppliedInitialValuesRef.current || !initialValues) return;
|
|
1428
1484
|
const trimmedEmail = initialValues.customer?.email?.trim();
|
|
@@ -2248,21 +2304,27 @@ export function NewBookingFlow({
|
|
|
2248
2304
|
totalPrice,
|
|
2249
2305
|
]);
|
|
2250
2306
|
|
|
2251
|
-
// Auto-select
|
|
2307
|
+
// Auto-select pickup when date is selected (or when slots load after date pick).
|
|
2252
2308
|
useEffect(() => {
|
|
2253
|
-
if (selectedDate
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2309
|
+
if (!selectedDate || timesForSelectedDate.length === 0) return;
|
|
2310
|
+
if (
|
|
2311
|
+
selectedAvailability &&
|
|
2312
|
+
availabilityMatchesSelectedDate(selectedAvailability, selectedDate, companyTimezone)
|
|
2313
|
+
) {
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
const toSelect = pickDefaultAvailabilityForTimes(timesForSelectedDate, activeOptions);
|
|
2317
|
+
if (toSelect) {
|
|
2318
|
+
setSelectedAvailability(toSelect);
|
|
2319
|
+
setError('');
|
|
2264
2320
|
}
|
|
2265
|
-
}, [
|
|
2321
|
+
}, [
|
|
2322
|
+
selectedDate,
|
|
2323
|
+
timesForSelectedDateSelectionKey,
|
|
2324
|
+
activeOptions,
|
|
2325
|
+
selectedAvailability,
|
|
2326
|
+
companyTimezone,
|
|
2327
|
+
]);
|
|
2266
2328
|
|
|
2267
2329
|
// Currency change does NOT trigger a refetch. Backend returns per-currency data (priceByCurrency,
|
|
2268
2330
|
// changeByCurrency, feesByCurrency, precomputedPrices, etc.) in one response; we just
|
|
@@ -2276,7 +2338,7 @@ export function NewBookingFlow({
|
|
|
2276
2338
|
avail.dateTime === selectedAvailability.dateTime &&
|
|
2277
2339
|
avail.productOptionId === selectedAvailability.productOptionId
|
|
2278
2340
|
);
|
|
2279
|
-
if (updatedAvailability) {
|
|
2341
|
+
if (updatedAvailability && shouldSyncSelectedAvailability(selectedAvailability, updatedAvailability)) {
|
|
2280
2342
|
setSelectedAvailability(updatedAvailability);
|
|
2281
2343
|
|
|
2282
2344
|
// Also update selectedReturnOption if it exists
|
|
@@ -2362,14 +2424,6 @@ export function NewBookingFlow({
|
|
|
2362
2424
|
}
|
|
2363
2425
|
}, [pricingConfig?.cancellationPolicies, cancellationPolicyId, currency]);
|
|
2364
2426
|
|
|
2365
|
-
const handleDateSelect = (date: string) => {
|
|
2366
|
-
if (date === selectedDate) return;
|
|
2367
|
-
setSelectedAvailability(null);
|
|
2368
|
-
setSelectedReturnOption(null);
|
|
2369
|
-
};
|
|
2370
|
-
|
|
2371
|
-
handleDateSelectRef.current = handleDateSelect;
|
|
2372
|
-
|
|
2373
2427
|
useEffect(() => {
|
|
2374
2428
|
if (flowUi?.autoSelectFirstAvailableDate !== true) return;
|
|
2375
2429
|
if (isPartialLaunch) return;
|
|
@@ -2388,7 +2442,10 @@ export function NewBookingFlow({
|
|
|
2388
2442
|
|
|
2389
2443
|
hasAutoSelectedPartnerDateRef.current = true;
|
|
2390
2444
|
setSelectedDate(first);
|
|
2391
|
-
|
|
2445
|
+
setSelectedReturnOption(null);
|
|
2446
|
+
setSelectedAvailability(
|
|
2447
|
+
pickDefaultAvailabilityForTimes(availabilitiesByDate[first] ?? [], activeOptions),
|
|
2448
|
+
);
|
|
2392
2449
|
if (!suppressCalendarDateScroll) {
|
|
2393
2450
|
setTimeout(() => {
|
|
2394
2451
|
const container = contentRef?.current;
|
|
@@ -2874,6 +2931,7 @@ export function NewBookingFlow({
|
|
|
2874
2931
|
customerEmail: email || undefined,
|
|
2875
2932
|
customerFirstName: firstName.trim() || undefined,
|
|
2876
2933
|
customerLastName: lastName.trim() || undefined,
|
|
2934
|
+
...optionalCheckoutContactFields(phoneNumber),
|
|
2877
2935
|
currency: currency,
|
|
2878
2936
|
travelerHotel: selectedPickupLocation?.name || undefined,
|
|
2879
2937
|
pickupLocationId: pickupLocationId || undefined,
|
|
@@ -2914,6 +2972,7 @@ export function NewBookingFlow({
|
|
|
2914
2972
|
customerEmail: email,
|
|
2915
2973
|
customerFirstName: firstName.trim() || undefined,
|
|
2916
2974
|
customerLastName: lastName.trim() || undefined,
|
|
2975
|
+
...optionalCheckoutContactFields(phoneNumber),
|
|
2917
2976
|
currency: currency,
|
|
2918
2977
|
reservationReference: reservation?.reservationReference,
|
|
2919
2978
|
travelerHotel: selectedPickupLocation?.name || undefined,
|
|
@@ -2945,6 +3004,7 @@ export function NewBookingFlow({
|
|
|
2945
3004
|
customerEmail: email || undefined,
|
|
2946
3005
|
customerFirstName: firstName.trim() || undefined,
|
|
2947
3006
|
customerLastName: lastName.trim() || undefined,
|
|
3007
|
+
...optionalCheckoutContactFields(phoneNumber),
|
|
2948
3008
|
currency: currency,
|
|
2949
3009
|
travelerHotel: selectedPickupLocation?.name || undefined,
|
|
2950
3010
|
pickupLocationId: pickupLocationId || undefined,
|
|
@@ -3130,6 +3190,7 @@ export function NewBookingFlow({
|
|
|
3130
3190
|
customerEmail: email || undefined,
|
|
3131
3191
|
customerFirstName: firstName.trim() || undefined,
|
|
3132
3192
|
customerLastName: lastName.trim() || undefined,
|
|
3193
|
+
...optionalCheckoutContactFields(phoneNumber),
|
|
3133
3194
|
currency: currency,
|
|
3134
3195
|
travelerHotel: product.pickupLocations?.find(loc => loc.id === pickupLocationId)?.name || undefined,
|
|
3135
3196
|
pickupLocationId: pickupLocationId || undefined,
|
|
@@ -3317,8 +3378,18 @@ export function NewBookingFlow({
|
|
|
3317
3378
|
selectableSoldOutDate={null}
|
|
3318
3379
|
isLoading={loadingAvailabilities || isFetchingMoreAvailabilities}
|
|
3319
3380
|
onDateSelect={(date) => {
|
|
3381
|
+
const dateChanged = date !== selectedDate;
|
|
3320
3382
|
setSelectedDate(date);
|
|
3321
|
-
|
|
3383
|
+
if (dateChanged) {
|
|
3384
|
+
setSelectedReturnOption(null);
|
|
3385
|
+
setSelectedAvailability(
|
|
3386
|
+
pickDefaultAvailabilityForTimes(
|
|
3387
|
+
availabilitiesByDate[date] ?? [],
|
|
3388
|
+
activeOptions,
|
|
3389
|
+
),
|
|
3390
|
+
);
|
|
3391
|
+
setError('');
|
|
3392
|
+
}
|
|
3322
3393
|
if (suppressCalendarDateScroll) return;
|
|
3323
3394
|
// Scroll so calendar is almost at top and user sees the rest of the booking flow.
|
|
3324
3395
|
// Dialog: scroll inside contentRef. Full-page: fall back to window scroll.
|
|
@@ -3406,8 +3477,12 @@ export function NewBookingFlow({
|
|
|
3406
3477
|
/>
|
|
3407
3478
|
)}
|
|
3408
3479
|
|
|
3409
|
-
{/* Select return time */}
|
|
3410
|
-
{
|
|
3480
|
+
{/* Select return time — wait for hydrated day details so summary refetches do not flash this block */}
|
|
3481
|
+
{selectedAvailabilityMatchesDate &&
|
|
3482
|
+
selectedAvailability &&
|
|
3483
|
+
!selectedAvailability.isSummary &&
|
|
3484
|
+
selectedAvailability.returnOptions &&
|
|
3485
|
+
selectedAvailability.returnOptions.length > 0 && (
|
|
3411
3486
|
<ReturnTimeSelector
|
|
3412
3487
|
returnOptions={returnOptionsWithFloor}
|
|
3413
3488
|
selectedReturnOption={selectedReturnOptionWithFloor}
|
|
@@ -3438,7 +3513,10 @@ export function NewBookingFlow({
|
|
|
3438
3513
|
: null}
|
|
3439
3514
|
|
|
3440
3515
|
{/* Cancellation policy selection - all options from config, sorted by cheapest first. Also show when forced by promo. */}
|
|
3441
|
-
{
|
|
3516
|
+
{selectedAvailabilityMatchesDate &&
|
|
3517
|
+
selectedAvailability &&
|
|
3518
|
+
((pricingConfig?.cancellationPolicies?.length ?? 0) > 0 || forcedCancellationPolicy) &&
|
|
3519
|
+
(() => {
|
|
3442
3520
|
const sortedPolicies = [...(pricingConfig?.cancellationPolicies ?? [])].sort((a, b) => {
|
|
3443
3521
|
const feeA = a.feeByCurrency[currency] ?? 0;
|
|
3444
3522
|
const feeB = b.feeByCurrency[currency] ?? 0;
|
|
@@ -3446,6 +3524,7 @@ export function NewBookingFlow({
|
|
|
3446
3524
|
});
|
|
3447
3525
|
return (
|
|
3448
3526
|
<div ref={cancellationPolicyRef}>
|
|
3527
|
+
{beforeCancellationPolicy}
|
|
3449
3528
|
<CancellationPolicySelector
|
|
3450
3529
|
policies={sortedPolicies}
|
|
3451
3530
|
selectedPolicyId={cancellationPolicyId}
|
|
@@ -3454,6 +3533,7 @@ export function NewBookingFlow({
|
|
|
3454
3533
|
t={t}
|
|
3455
3534
|
onPolicySelect={setCancellationPolicyId}
|
|
3456
3535
|
forcedPolicy={forcedCancellationPolicy}
|
|
3536
|
+
sectionLabel={flowUi?.cancellationPolicySectionLabel}
|
|
3457
3537
|
/>
|
|
3458
3538
|
</div>
|
|
3459
3539
|
);
|
|
@@ -3552,6 +3632,8 @@ export function NewBookingFlow({
|
|
|
3552
3632
|
onFirstNameChange={(v) => { setFirstName(v); setError(''); }}
|
|
3553
3633
|
onLastNameChange={(v) => { setLastName(v); setError(''); }}
|
|
3554
3634
|
onEmailChange={(v) => { setEmail(v); setError(''); }}
|
|
3635
|
+
phoneNumber={phoneNumber}
|
|
3636
|
+
onPhoneNumberChange={(v) => { setPhoneNumber(v); setError(''); }}
|
|
3555
3637
|
readOnlyContactFields={false}
|
|
3556
3638
|
pickupLocations={
|
|
3557
3639
|
selectedDate && product.pickupLocations && product.pickupLocations.length > 0
|
|
@@ -324,6 +324,13 @@
|
|
|
324
324
|
color: #b91c1c;
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
+
.contactFieldHint {
|
|
328
|
+
margin: 0.625rem 0 0;
|
|
329
|
+
font-size: 0.75rem;
|
|
330
|
+
line-height: 1.35;
|
|
331
|
+
color: var(--booking-stone-500, #78716c);
|
|
332
|
+
}
|
|
333
|
+
|
|
327
334
|
.addOnOptionBtn {
|
|
328
335
|
display: flex;
|
|
329
336
|
align-items: center;
|
|
@@ -60,6 +60,8 @@ import {
|
|
|
60
60
|
} from '../../lib/booking/source-metadata';
|
|
61
61
|
import type { VideoSources } from '../../constants/products';
|
|
62
62
|
import type { BookingFlowUiOptions } from './booking-flow-ui';
|
|
63
|
+
import { optionalCheckoutContactFields } from '../../lib/booking/checkout-contact';
|
|
64
|
+
import { CheckoutOptionalPhoneFields } from './CheckoutOptionalPhoneFields';
|
|
63
65
|
import type {
|
|
64
66
|
ProviderDashboardChangeBookingPayload,
|
|
65
67
|
ProviderDashboardInitialBooking,
|
|
@@ -193,6 +195,7 @@ export function PrivateShuttleBookingFlow({
|
|
|
193
195
|
const [email, setEmail] = useState('');
|
|
194
196
|
const [firstName, setFirstName] = useState('');
|
|
195
197
|
const [lastName, setLastName] = useState('');
|
|
198
|
+
const [phoneNumber, setPhoneNumber] = useState('');
|
|
196
199
|
const [pickupLocationId, setPickupLocationId] = useState<string | null>(
|
|
197
200
|
() => initialBooking?.pickupLocationId ?? initialValues?.pickupLocationId ?? null
|
|
198
201
|
);
|
|
@@ -1684,6 +1687,8 @@ export function PrivateShuttleBookingFlow({
|
|
|
1684
1687
|
customerEmail: email || undefined,
|
|
1685
1688
|
customerFirstName: firstName.trim() || undefined,
|
|
1686
1689
|
customerLastName: lastName.trim() || undefined,
|
|
1690
|
+
...optionalCheckoutContactFields(phoneNumber),
|
|
1691
|
+
...optionalCheckoutContactFields(phoneNumber),
|
|
1687
1692
|
currency,
|
|
1688
1693
|
travelerHotel: selectedPickupLocation?.name || customPickupAddress || undefined,
|
|
1689
1694
|
pickupLocationId: pickupLocationId || undefined,
|
|
@@ -1724,6 +1729,8 @@ export function PrivateShuttleBookingFlow({
|
|
|
1724
1729
|
customerEmail: email,
|
|
1725
1730
|
customerFirstName: firstName.trim() || undefined,
|
|
1726
1731
|
customerLastName: lastName.trim() || undefined,
|
|
1732
|
+
...optionalCheckoutContactFields(phoneNumber),
|
|
1733
|
+
...optionalCheckoutContactFields(phoneNumber),
|
|
1727
1734
|
currency,
|
|
1728
1735
|
reservationReference: reservation.reservationReference,
|
|
1729
1736
|
travelerHotel: selectedPickupLocation?.name || customPickupAddress || undefined,
|
|
@@ -1804,6 +1811,7 @@ export function PrivateShuttleBookingFlow({
|
|
|
1804
1811
|
customerEmail: email,
|
|
1805
1812
|
customerFirstName: firstName.trim() || undefined,
|
|
1806
1813
|
customerLastName: lastName.trim() || undefined,
|
|
1814
|
+
...optionalCheckoutContactFields(phoneNumber),
|
|
1807
1815
|
currency,
|
|
1808
1816
|
reservationReference: reservation.reservationReference,
|
|
1809
1817
|
travelerHotel: selectedPickupLocation?.name || customPickupAddress || undefined,
|
|
@@ -1835,6 +1843,8 @@ export function PrivateShuttleBookingFlow({
|
|
|
1835
1843
|
customerEmail: email || undefined,
|
|
1836
1844
|
customerFirstName: firstName.trim() || undefined,
|
|
1837
1845
|
customerLastName: lastName.trim() || undefined,
|
|
1846
|
+
...optionalCheckoutContactFields(phoneNumber),
|
|
1847
|
+
...optionalCheckoutContactFields(phoneNumber),
|
|
1838
1848
|
currency,
|
|
1839
1849
|
travelerHotel: selectedPickupLocation?.name || customPickupAddress || undefined,
|
|
1840
1850
|
pickupLocationId: pickupLocationId || undefined,
|
|
@@ -1990,6 +2000,7 @@ export function PrivateShuttleBookingFlow({
|
|
|
1990
2000
|
customerEmail: email || undefined,
|
|
1991
2001
|
customerFirstName: firstName.trim() || undefined,
|
|
1992
2002
|
customerLastName: lastName.trim() || undefined,
|
|
2003
|
+
...optionalCheckoutContactFields(phoneNumber),
|
|
1993
2004
|
currency,
|
|
1994
2005
|
travelerHotel: selectedPickupLocation?.name || customPickupAddress || undefined,
|
|
1995
2006
|
pickupLocationId: pickupLocationId || undefined,
|
|
@@ -2875,6 +2886,16 @@ export function PrivateShuttleBookingFlow({
|
|
|
2875
2886
|
className={styles.input}
|
|
2876
2887
|
/>
|
|
2877
2888
|
</div>
|
|
2889
|
+
<CheckoutOptionalPhoneFields
|
|
2890
|
+
idPrefix="shuttle"
|
|
2891
|
+
labelClassName={styles.sectionLabel}
|
|
2892
|
+
phoneNumber={phoneNumber}
|
|
2893
|
+
onPhoneNumberChange={(v) => {
|
|
2894
|
+
setPhoneNumber(v);
|
|
2895
|
+
setError('');
|
|
2896
|
+
}}
|
|
2897
|
+
t={t}
|
|
2898
|
+
/>
|
|
2878
2899
|
</>
|
|
2879
2900
|
{pricingConfig?.cancellationPolicies &&
|
|
2880
2901
|
pricingConfig.cancellationPolicies.length > 0 && (
|
|
@@ -107,6 +107,8 @@ export interface BookingFlowBaseProps {
|
|
|
107
107
|
* a date and shuttle time are selected. Used by photo-first dependent add-on flows.
|
|
108
108
|
*/
|
|
109
109
|
afterItinerary?: ReactNode | ((context: BookingFlowAfterItineraryContext) => ReactNode);
|
|
110
|
+
/** Optional note rendered immediately above shuttle cancellation policy options (e.g. photo DAP). */
|
|
111
|
+
beforeCancellationPolicy?: ReactNode;
|
|
110
112
|
/**
|
|
111
113
|
* Optional hook to insert add-on rows into the Build Itinerary box. Receives the
|
|
112
114
|
* base itinerary from the selected shuttle and returns the display list to render.
|
|
@@ -67,6 +67,8 @@ export interface BookingFlowUiOptions {
|
|
|
67
67
|
itineraryStickyTopOffsetPx?: number;
|
|
68
68
|
/** Provider dashboard change flow: quote/override panel shown immediately before confirm CTA. */
|
|
69
69
|
providerDashboardChangePricingUi?: ProviderDashboardChangePricingUi;
|
|
70
|
+
/** Override the orange section heading above cancellation policy cards (e.g. photo-first shuttle flow). */
|
|
71
|
+
cancellationPolicySectionLabel?: string;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
/**
|
|
@@ -273,12 +273,20 @@
|
|
|
273
273
|
border-radius: 0.5rem;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
.booking-flow-preflight input:focus {
|
|
276
|
+
.booking-flow-preflight input:focus:not(.react-international-phone-input) {
|
|
277
277
|
outline: none;
|
|
278
278
|
border-color: var(--booking-primary);
|
|
279
279
|
box-shadow: 0 0 0 2px rgba(5, 150, 105, 0.2);
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
/* Phone country widget: single focus ring on outer container, not inner tel input */
|
|
283
|
+
.booking-flow-preflight input.react-international-phone-input:focus,
|
|
284
|
+
.booking-flow-preflight input.react-international-phone-input:focus-visible {
|
|
285
|
+
outline: none !important;
|
|
286
|
+
border: none !important;
|
|
287
|
+
box-shadow: none !important;
|
|
288
|
+
}
|
|
289
|
+
|
|
282
290
|
/**
|
|
283
291
|
* Admin “custom receipt line” row: scoped preflight sets `input, button { padding: 0 }` with
|
|
284
292
|
* higher specificity than Tailwind padding utilities, so insets must be restored here.
|
|
@@ -1012,9 +1020,69 @@
|
|
|
1012
1020
|
border-top-color: var(--booking-stone-200, #e7e5e4) !important;
|
|
1013
1021
|
}
|
|
1014
1022
|
|
|
1015
|
-
/* ========== Input fields - padding ========== */
|
|
1016
|
-
.booking-flow-preflight [class*="CheckoutForm_input"] {
|
|
1017
|
-
padding: 0.
|
|
1023
|
+
/* ========== Input fields - padding (scope to <input> only — [class*="CheckoutForm_input"] also matches inputError) ========== */
|
|
1024
|
+
.booking-flow-preflight input[class*="CheckoutForm_input"] {
|
|
1025
|
+
padding: 0.5rem 0.625rem !important;
|
|
1026
|
+
font-size: 0.875rem !important;
|
|
1027
|
+
line-height: 1.25rem !important;
|
|
1028
|
+
background-color: var(--booking-stone-50, #fafaf9) !important;
|
|
1029
|
+
border: 1px solid var(--booking-stone-300, #d6d3d1) !important;
|
|
1030
|
+
border-radius: 0.5rem !important;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
.booking-flow-preflight input[class*="CheckoutForm_input"]:focus {
|
|
1034
|
+
border-color: var(--booking-primary, #059669) !important;
|
|
1035
|
+
background-color: #fff !important;
|
|
1036
|
+
outline: none !important;
|
|
1037
|
+
box-shadow: 0 0 0 2px rgba(5, 150, 105, 0.2) !important;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
.booking-flow-preflight [class*="CheckoutForm_checkoutPhoneInput"] :global(.react-international-phone-input-container) {
|
|
1041
|
+
border: 1px solid var(--booking-stone-300, #d6d3d1) !important;
|
|
1042
|
+
border-radius: 0.5rem !important;
|
|
1043
|
+
background-color: var(--booking-stone-50, #fafaf9) !important;
|
|
1044
|
+
box-shadow: none !important;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
.booking-flow-preflight [class*="CheckoutForm_checkoutPhoneInput"]:focus-within :global(.react-international-phone-input-container) {
|
|
1048
|
+
border-color: var(--booking-primary, #059669) !important;
|
|
1049
|
+
border-width: 1px !important;
|
|
1050
|
+
background-color: #fff !important;
|
|
1051
|
+
outline: none !important;
|
|
1052
|
+
box-shadow: 0 0 0 2px rgba(5, 150, 105, 0.2) !important;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
.booking-flow-preflight [class*="CheckoutForm_checkoutPhoneInput"] :global(.react-international-phone-input) {
|
|
1056
|
+
padding: 0.5rem 0.625rem 0.5rem 0.25rem !important;
|
|
1057
|
+
font-size: 0.875rem !important;
|
|
1058
|
+
border-radius: 0 !important;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
.booking-flow-preflight [class*="CheckoutForm_checkoutPhoneInput"]:focus-within :global(.react-international-phone-country-selector-button) {
|
|
1062
|
+
box-shadow: none !important;
|
|
1063
|
+
outline: none !important;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/* Hint is a div (preflight zeros all `p` margins) */
|
|
1067
|
+
.booking-flow-preflight [class*="CheckoutForm_optionalPhoneFieldHint"],
|
|
1068
|
+
.booking-flow-root [class*="CheckoutForm_optionalPhoneFieldHint"] {
|
|
1069
|
+
margin: 0 !important;
|
|
1070
|
+
padding: 0.375rem 0.5rem 0 !important;
|
|
1071
|
+
font-size: 0.75rem !important;
|
|
1072
|
+
line-height: 1.4 !important;
|
|
1073
|
+
color: var(--booking-stone-500, #78716c) !important;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
.booking-flow-preflight [class*="CheckoutForm_inputError"],
|
|
1077
|
+
.booking-flow-root [class*="CheckoutForm_inputError"] {
|
|
1078
|
+
margin: 0.375rem 0 0 !important;
|
|
1079
|
+
padding: 0 !important;
|
|
1080
|
+
border: none !important;
|
|
1081
|
+
background: transparent !important;
|
|
1082
|
+
box-shadow: none !important;
|
|
1083
|
+
font-size: 0.75rem !important;
|
|
1084
|
+
line-height: 1.35 !important;
|
|
1085
|
+
color: #b91c1c !important;
|
|
1018
1086
|
}
|
|
1019
1087
|
/* Promo field: scope to <input> only — [class*="PromoCodeInput_input"] also matches inputWrap (…inputWrap…) and was drawing a second box */
|
|
1020
1088
|
.booking-flow-preflight input[class*="PromoCodeInput_input"] {
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
{
|
|
11
11
|
"title": "30-minute session",
|
|
12
12
|
"items": [
|
|
13
|
-
"📸 20 to 25 minutes of photography time",
|
|
14
13
|
"📍 2 Locations within walking distance",
|
|
15
14
|
"🖼️ 25 professionally edited photos delivered in a personalized online gallery"
|
|
16
15
|
]
|
|
@@ -18,7 +17,6 @@
|
|
|
18
17
|
{
|
|
19
18
|
"title": "60-minute session",
|
|
20
19
|
"items": [
|
|
21
|
-
"📸 50 to 55 minutes of photography time",
|
|
22
20
|
"📍 2-3 Locations within walking distance",
|
|
23
21
|
"🖼️ 50 professionally edited photos delivered in a personalized online gallery"
|
|
24
22
|
]
|
|
@@ -26,7 +24,6 @@
|
|
|
26
24
|
{
|
|
27
25
|
"title": "90-minute session",
|
|
28
26
|
"items": [
|
|
29
|
-
"📸 80-90 mins of Photography Time",
|
|
30
27
|
"📍 5 Locations within walking distance",
|
|
31
28
|
"🖼️ 75 professionally edited photos delivered in a personalized online gallery"
|
|
32
29
|
]
|
|
@@ -7,27 +7,27 @@
|
|
|
7
7
|
"title": "What's included",
|
|
8
8
|
"content": [
|
|
9
9
|
{
|
|
10
|
-
"title": "
|
|
10
|
+
"title": "Alpine Vows - 1 hour",
|
|
11
11
|
"items": [
|
|
12
|
-
"📸
|
|
13
|
-
"📍 2 Locations within walking distance",
|
|
14
|
-
"🖼️
|
|
12
|
+
"📸 60 minutes of photography time",
|
|
13
|
+
"📍 2-3 Locations within walking distance",
|
|
14
|
+
"🖼️ 60+ professionally edited photos delivered in a personalized online gallery"
|
|
15
15
|
]
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
|
-
"title": "
|
|
18
|
+
"title": "The Summit Story - 4 hours",
|
|
19
19
|
"items": [
|
|
20
|
-
"📸
|
|
21
|
-
"📍
|
|
22
|
-
"🖼️
|
|
20
|
+
"📸 3-4 hours of photography time",
|
|
21
|
+
"📍 Up to 3 Local Locations",
|
|
22
|
+
"🖼️ 100+ professionally edited photos delivered in a personalized online gallery"
|
|
23
23
|
]
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
|
-
"title": "
|
|
26
|
+
"title": "The Grand Adventure - 8 hours",
|
|
27
27
|
"items": [
|
|
28
|
-
"📸
|
|
29
|
-
"📍
|
|
30
|
-
"🖼️
|
|
28
|
+
"📸 7-8 hours of photography time",
|
|
29
|
+
"📍 Up to 5 Local Locations",
|
|
30
|
+
"🖼️ 100+ professionally edited photos delivered in a personalized online gallery"
|
|
31
31
|
]
|
|
32
32
|
},
|
|
33
33
|
{
|