@ticketboothapp/booking 1.2.100 → 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 +310 -1
- 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
|
@@ -7,27 +7,24 @@
|
|
|
7
7
|
"title": "What's included",
|
|
8
8
|
"content": [
|
|
9
9
|
{
|
|
10
|
-
"title": "
|
|
10
|
+
"title": "Essentials - 30 minutes",
|
|
11
11
|
"items": [
|
|
12
|
-
"📸 20 to 25 minutes of photography time",
|
|
13
12
|
"📍 2 Locations within walking distance",
|
|
14
|
-
"🖼️
|
|
13
|
+
"🖼️ 30 professionally edited photos delivered in a personalized online gallery"
|
|
15
14
|
]
|
|
16
15
|
},
|
|
17
16
|
{
|
|
18
|
-
"title": "
|
|
17
|
+
"title": "Celebration - 60 minutes",
|
|
19
18
|
"items": [
|
|
20
|
-
"📸 50 to 55 minutes of photography time",
|
|
21
19
|
"📍 2-3 Locations within walking distance",
|
|
22
|
-
"🖼️
|
|
20
|
+
"🖼️ 60 professionally edited photos delivered in a personalized online gallery"
|
|
23
21
|
]
|
|
24
22
|
},
|
|
25
23
|
{
|
|
26
|
-
"title": "
|
|
24
|
+
"title": "Immersive - 90 minutes",
|
|
27
25
|
"items": [
|
|
28
|
-
"📸 80-90 mins of Photography Time",
|
|
29
26
|
"📍 5 Locations within walking distance",
|
|
30
|
-
"🖼️
|
|
27
|
+
"🖼️ 90 professionally edited photos delivered in a personalized online gallery"
|
|
31
28
|
]
|
|
32
29
|
},
|
|
33
30
|
{
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
{
|
|
7
7
|
"productId": "p_qa5keidRoV6H",
|
|
8
8
|
"optionIds": ["po_cSD2wlJfQqc0"],
|
|
9
|
+
"dependentAddOnProductIds": [
|
|
10
|
+
"dap_sM8pSzbFfU7P",
|
|
11
|
+
"dap_6tITweGzVyg8",
|
|
12
|
+
"dap_Xy3OCPIKQ7vr"
|
|
13
|
+
],
|
|
9
14
|
"display": {
|
|
10
15
|
"path": "/moraine-lake-shuttle",
|
|
11
16
|
"slug": "moraine-lake-sunrise-lake-louise-golden-hour",
|
|
@@ -20,6 +25,11 @@
|
|
|
20
25
|
{
|
|
21
26
|
"productId": "p_YKQsLmKhfYFp",
|
|
22
27
|
"optionIds": ["po_82wA2gUZBZ9z"],
|
|
28
|
+
"dependentAddOnProductIds": [
|
|
29
|
+
"dap_sM8pSzbFfU7P",
|
|
30
|
+
"dap_6tITweGzVyg8",
|
|
31
|
+
"dap_Xy3OCPIKQ7vr"
|
|
32
|
+
],
|
|
23
33
|
"display": {
|
|
24
34
|
"path": "/moraine-lake-shuttle",
|
|
25
35
|
"slug": "moraine-lake-sunrise",
|
|
@@ -33,6 +43,11 @@
|
|
|
33
43
|
{
|
|
34
44
|
"productId": "p_CUJ4sVvaOkjk",
|
|
35
45
|
"optionIds": ["po_ImWip6rbg0D6", "po_lOO7wz6uMus1", "po_fvrAfalgI673"],
|
|
46
|
+
"dependentAddOnProductIds": [
|
|
47
|
+
"dap_sM8pSzbFfU7P",
|
|
48
|
+
"dap_6tITweGzVyg8",
|
|
49
|
+
"dap_Xy3OCPIKQ7vr"
|
|
50
|
+
],
|
|
36
51
|
"display": {
|
|
37
52
|
"path": "/moraine-lake-shuttle",
|
|
38
53
|
"slug": "two-lakes-combo",
|
|
@@ -47,6 +62,11 @@
|
|
|
47
62
|
{
|
|
48
63
|
"productId": "p_wQHE15ITi7TS",
|
|
49
64
|
"optionIds": ["po_FTLSMsahWKjM", "po_nbp2pFQo9AH7", "po_nzlCuOVJU181"],
|
|
65
|
+
"dependentAddOnProductIds": [
|
|
66
|
+
"dap_sM8pSzbFfU7P",
|
|
67
|
+
"dap_6tITweGzVyg8",
|
|
68
|
+
"dap_Xy3OCPIKQ7vr"
|
|
69
|
+
],
|
|
50
70
|
"display": {
|
|
51
71
|
"path": "/moraine-lake-shuttle",
|
|
52
72
|
"slug": "moraine-lake-adventure",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { normalizeOptionalPhoneNumber } from './phone';
|
|
2
|
+
|
|
3
|
+
/** Optional phone from checkout — omitted when blank or country-code-only (e.g. "+1"). */
|
|
4
|
+
export function optionalCheckoutContactFields(phoneNumber: string) {
|
|
5
|
+
return {
|
|
6
|
+
customerPhoneNumber: normalizeOptionalPhoneNumber(phoneNumber),
|
|
7
|
+
};
|
|
8
|
+
}
|
|
@@ -102,6 +102,12 @@
|
|
|
102
102
|
"firstNamePlaceholder": "Jane",
|
|
103
103
|
"lastName": "Last Name",
|
|
104
104
|
"lastNamePlaceholder": "Smith",
|
|
105
|
+
"phoneOptional": "Phone",
|
|
106
|
+
"phoneOptionalPlaceholder": "e.g. +1 403 555 0123",
|
|
107
|
+
"phoneOptionalHint": "For pickup and day-of updates",
|
|
108
|
+
"whatsAppOptional": "WhatsApp",
|
|
109
|
+
"whatsAppOptionalPlaceholder": "e.g. +1 403 555 0123",
|
|
110
|
+
"whatsAppOptionalHint": "Leave blank if same as phone",
|
|
105
111
|
"dapNoLastNameOnFile": "We couldn’t find a last name on this booking in our system. Please contact us, or use Manage booking with the last name from your confirmation email.",
|
|
106
112
|
"dapCancellationPolicyHeading": "Cancellation policy",
|
|
107
113
|
"dapCancellationPolicyBody": "You may cancel this add-on for a full refund up to {days} {daysUnit} before your scheduled photo session. Cancellations made after that deadline may forfeit the entire add-on cost.",
|
|
@@ -102,6 +102,12 @@
|
|
|
102
102
|
"firstNamePlaceholder": "Jeanne",
|
|
103
103
|
"lastName": "Nom de famille",
|
|
104
104
|
"lastNamePlaceholder": "Dupont",
|
|
105
|
+
"phoneOptional": "Téléphone",
|
|
106
|
+
"phoneOptionalPlaceholder": "p. ex. +1 403 555 0123",
|
|
107
|
+
"phoneOptionalHint": "Pour la prise en charge et les mises à jour le jour même",
|
|
108
|
+
"whatsAppOptional": "WhatsApp",
|
|
109
|
+
"whatsAppOptionalPlaceholder": "p. ex. +1 403 555 0123",
|
|
110
|
+
"whatsAppOptionalHint": "Laissez vide si identique au téléphone",
|
|
105
111
|
"dapNoLastNameOnFile": "Nous n’avons pas trouvé de nom de famille pour cette réservation. Contactez-nous, ou utilisez Gérer la réservation avec le nom figurant sur votre courriel de confirmation.",
|
|
106
112
|
"dapCancellationPolicyHeading": "Politique d’annulation",
|
|
107
113
|
"dapCancellationPolicyBody": "Vous pouvez annuler cette option avec remboursement intégral jusqu’à {days} {daysUnit} avant votre séance photo prévue. Toute annulation après cette date peut entraîner la perte du montant total de l’option.",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Minimum digit count for a stored phone (country code + national number). */
|
|
2
|
+
const MIN_PHONE_DIGITS = 10;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Treat empty input and country-code-only values (e.g. default "+1") as no phone.
|
|
6
|
+
* Returns the trimmed E.164-style string when meaningful, otherwise undefined.
|
|
7
|
+
*/
|
|
8
|
+
export function normalizeOptionalPhoneNumber(raw: string | undefined | null): string | undefined {
|
|
9
|
+
const trimmed = (raw ?? '').trim();
|
|
10
|
+
if (!trimmed) return undefined;
|
|
11
|
+
const digits = trimmed.replace(/\D/g, '');
|
|
12
|
+
if (digits.length < MIN_PHONE_DIGITS) return undefined;
|
|
13
|
+
return trimmed;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function hasMeaningfulPhoneNumber(raw: string | undefined | null): boolean {
|
|
17
|
+
return normalizeOptionalPhoneNumber(raw) !== undefined;
|
|
18
|
+
}
|
package/src/lib/booking-api.ts
CHANGED
|
@@ -355,26 +355,304 @@ function getAuthHeaders(): Record<string, string> {
|
|
|
355
355
|
|
|
356
356
|
/** Idempotent GET retries: transient network errors and 502/503/504 (new trace span each attempt). */
|
|
357
357
|
const BOOKING_GET_MAX_RETRIES = 2;
|
|
358
|
+
const BOOKING_ATTEMPT_HEADER = 'X-Booking-Attempt-Id';
|
|
359
|
+
const SLOW_REQUEST_THRESHOLD_MS = 2500;
|
|
360
|
+
const SUCCESS_TIMING_SAMPLE_RATE = 0.02;
|
|
361
|
+
const NETWORK_FAILURE_PROBE_TIMEOUT_MS = 1500;
|
|
358
362
|
|
|
359
363
|
function sleep(ms: number): Promise<void> {
|
|
360
364
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
361
365
|
}
|
|
362
366
|
|
|
367
|
+
function newBookingAttemptId(): string {
|
|
368
|
+
try {
|
|
369
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
370
|
+
return crypto.randomUUID();
|
|
371
|
+
}
|
|
372
|
+
} catch {
|
|
373
|
+
/* fall through */
|
|
374
|
+
}
|
|
375
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 12)}`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function getEndpointFromUrl(url: string): string {
|
|
379
|
+
try {
|
|
380
|
+
const parsed = new URL(url, typeof window !== 'undefined' ? window.location.href : API_BASE);
|
|
381
|
+
return parsed.pathname;
|
|
382
|
+
} catch {
|
|
383
|
+
return '';
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function isBookingCriticalEndpoint(endpoint: string): boolean {
|
|
388
|
+
return (
|
|
389
|
+
endpoint === '/1/get-availabilities' ||
|
|
390
|
+
endpoint === '/1/products' ||
|
|
391
|
+
endpoint === '/1/reserve' ||
|
|
392
|
+
endpoint.startsWith('/1/companies/')
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function getConnectionDiagnostics(): Record<string, number | string | boolean> | null {
|
|
397
|
+
if (typeof navigator === 'undefined') return null;
|
|
398
|
+
const connection = (
|
|
399
|
+
navigator as Navigator & {
|
|
400
|
+
connection?: {
|
|
401
|
+
effectiveType?: string;
|
|
402
|
+
rtt?: number;
|
|
403
|
+
downlink?: number;
|
|
404
|
+
saveData?: boolean;
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
).connection;
|
|
408
|
+
if (!connection) return null;
|
|
409
|
+
return {
|
|
410
|
+
effectiveType: connection.effectiveType ?? 'unknown',
|
|
411
|
+
rtt: typeof connection.rtt === 'number' ? connection.rtt : -1,
|
|
412
|
+
downlink: typeof connection.downlink === 'number' ? connection.downlink : -1,
|
|
413
|
+
saveData: Boolean(connection.saveData),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function getBrowserDiagnostics(): Record<string, number | string | boolean> {
|
|
418
|
+
if (typeof window === 'undefined') return {};
|
|
419
|
+
return {
|
|
420
|
+
siteOrigin: window.location.origin,
|
|
421
|
+
siteProtocol: window.location.protocol,
|
|
422
|
+
isSecureContext: window.isSecureContext,
|
|
423
|
+
referrer: document.referrer || '',
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function getAvailabilityQueryShape(url: string): Record<string, string | boolean | string[]> | null {
|
|
428
|
+
try {
|
|
429
|
+
const parsed = new URL(url, typeof window !== 'undefined' ? window.location.href : API_BASE);
|
|
430
|
+
if (parsed.pathname !== '/1/get-availabilities') return null;
|
|
431
|
+
const params = parsed.searchParams;
|
|
432
|
+
return {
|
|
433
|
+
queryKeys: Array.from(params.keys()).sort(),
|
|
434
|
+
productId: params.get('productId') ?? '',
|
|
435
|
+
productOptionId: params.get('productOptionId') ?? '',
|
|
436
|
+
startDate: params.get('startDate') ?? '',
|
|
437
|
+
endDate: params.get('endDate') ?? '',
|
|
438
|
+
fromDateTime: params.get('fromDateTime') ?? '',
|
|
439
|
+
toDateTime: params.get('toDateTime') ?? '',
|
|
440
|
+
allOptions: params.get('allOptions') === 'true',
|
|
441
|
+
summary: params.get('summary') === 'true',
|
|
442
|
+
};
|
|
443
|
+
} catch {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function latestResourceTiming(url: string): Record<string, number | string | boolean> | null {
|
|
449
|
+
if (typeof performance === 'undefined') return null;
|
|
450
|
+
const entry = performance
|
|
451
|
+
.getEntriesByName(url)
|
|
452
|
+
.filter((item): item is PerformanceResourceTiming => item.entryType === 'resource')
|
|
453
|
+
.at(-1);
|
|
454
|
+
if (!entry) return null;
|
|
455
|
+
return {
|
|
456
|
+
name: entry.name,
|
|
457
|
+
durationMs: Math.round(entry.duration),
|
|
458
|
+
startTimeMs: Math.round(entry.startTime),
|
|
459
|
+
fetchStartMs: Math.round(entry.fetchStart),
|
|
460
|
+
domainLookupStartMs: Math.round(entry.domainLookupStart),
|
|
461
|
+
domainLookupEndMs: Math.round(entry.domainLookupEnd),
|
|
462
|
+
connectStartMs: Math.round(entry.connectStart),
|
|
463
|
+
connectEndMs: Math.round(entry.connectEnd),
|
|
464
|
+
secureConnectionStartMs: Math.round(entry.secureConnectionStart),
|
|
465
|
+
requestStartMs: Math.round(entry.requestStart),
|
|
466
|
+
responseStartMs: Math.round(entry.responseStart),
|
|
467
|
+
responseEndMs: Math.round(entry.responseEnd),
|
|
468
|
+
transferSize: entry.transferSize,
|
|
469
|
+
encodedBodySize: entry.encodedBodySize,
|
|
470
|
+
decodedBodySize: entry.decodedBodySize,
|
|
471
|
+
timingRestricted: entry.responseStart === 0 && entry.responseEnd === 0,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function resourceTimingStatus(
|
|
476
|
+
timing: Record<string, number | string | boolean> | null
|
|
477
|
+
): string {
|
|
478
|
+
if (!timing) return 'missing';
|
|
479
|
+
if (timing.timingRestricted === true) return 'timing_restricted';
|
|
480
|
+
if (typeof timing.responseStartMs === 'number' && timing.responseStartMs > 0) return 'response_started';
|
|
481
|
+
if (typeof timing.requestStartMs === 'number' && timing.requestStartMs > 0) return 'request_started';
|
|
482
|
+
if (typeof timing.connectStartMs === 'number' && timing.connectStartMs > 0) return 'connect_started';
|
|
483
|
+
if (typeof timing.fetchStartMs === 'number' && timing.fetchStartMs > 0) return 'fetch_started';
|
|
484
|
+
return 'unknown';
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function runNetworkFailureProbe(
|
|
488
|
+
label: string,
|
|
489
|
+
url: string,
|
|
490
|
+
init: RequestInit
|
|
491
|
+
): Promise<Record<string, number | string | boolean | null>> {
|
|
492
|
+
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
493
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
494
|
+
let timedOut = false;
|
|
495
|
+
const timeoutId =
|
|
496
|
+
controller && typeof window !== 'undefined'
|
|
497
|
+
? window.setTimeout(() => {
|
|
498
|
+
timedOut = true;
|
|
499
|
+
controller.abort();
|
|
500
|
+
}, NETWORK_FAILURE_PROBE_TIMEOUT_MS)
|
|
501
|
+
: null;
|
|
502
|
+
try {
|
|
503
|
+
const response = await fetch(url, {
|
|
504
|
+
...init,
|
|
505
|
+
cache: 'no-store',
|
|
506
|
+
signal: controller?.signal,
|
|
507
|
+
});
|
|
508
|
+
const timing = latestResourceTiming(url);
|
|
509
|
+
return {
|
|
510
|
+
label,
|
|
511
|
+
reachedFetchResponse: true,
|
|
512
|
+
ok: response.ok,
|
|
513
|
+
status: response.status,
|
|
514
|
+
responseType: response.type,
|
|
515
|
+
elapsedMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt),
|
|
516
|
+
timedOut,
|
|
517
|
+
resourceTimingStatus: resourceTimingStatus(timing),
|
|
518
|
+
};
|
|
519
|
+
} catch (error) {
|
|
520
|
+
const timing = latestResourceTiming(url);
|
|
521
|
+
return {
|
|
522
|
+
label,
|
|
523
|
+
reachedFetchResponse: false,
|
|
524
|
+
ok: false,
|
|
525
|
+
status: null,
|
|
526
|
+
elapsedMs: Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt),
|
|
527
|
+
timedOut,
|
|
528
|
+
errorName: error instanceof Error ? error.name : 'UnknownError',
|
|
529
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
530
|
+
resourceTimingStatus: resourceTimingStatus(timing),
|
|
531
|
+
};
|
|
532
|
+
} finally {
|
|
533
|
+
if (timeoutId != null) window.clearTimeout(timeoutId);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function collectNetworkFailureProbeResults(
|
|
538
|
+
endpoint: string,
|
|
539
|
+
correlationId: string,
|
|
540
|
+
traceparent: string
|
|
541
|
+
): Promise<Array<Record<string, number | string | boolean | null>>> {
|
|
542
|
+
if (typeof window === 'undefined') return [];
|
|
543
|
+
const cacheBust = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
544
|
+
return Promise.all([
|
|
545
|
+
runNetworkFailureProbe(
|
|
546
|
+
'same_origin_asset_head',
|
|
547
|
+
`${window.location.origin}/favicon.ico?tb_probe=${cacheBust}`,
|
|
548
|
+
{ method: 'HEAD', mode: 'same-origin' }
|
|
549
|
+
),
|
|
550
|
+
runNetworkFailureProbe(
|
|
551
|
+
'api_telemetry_cors_post',
|
|
552
|
+
`${API_BASE}/1/client-telemetry`,
|
|
553
|
+
{
|
|
554
|
+
method: 'POST',
|
|
555
|
+
mode: 'cors',
|
|
556
|
+
headers: {
|
|
557
|
+
'Content-Type': 'application/json',
|
|
558
|
+
[BOOKING_CORRELATION_HEADER]: correlationId,
|
|
559
|
+
traceparent,
|
|
560
|
+
},
|
|
561
|
+
body: JSON.stringify({
|
|
562
|
+
event: 'BOOKING_DIALOG_NETWORK_PROBE_PING',
|
|
563
|
+
endpoint,
|
|
564
|
+
correlationId,
|
|
565
|
+
traceparent,
|
|
566
|
+
apiBase: API_BASE,
|
|
567
|
+
pageUrl: window.location.href,
|
|
568
|
+
userAgent: window.navigator.userAgent,
|
|
569
|
+
online: window.navigator.onLine,
|
|
570
|
+
occurredAt: new Date().toISOString(),
|
|
571
|
+
...getBrowserDiagnostics(),
|
|
572
|
+
}),
|
|
573
|
+
}
|
|
574
|
+
),
|
|
575
|
+
]);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function bookingRequestTelemetryContext(
|
|
579
|
+
url: string,
|
|
580
|
+
endpoint: string,
|
|
581
|
+
headers: Record<string, string>,
|
|
582
|
+
requestAttemptId: string,
|
|
583
|
+
attemptNumber: number,
|
|
584
|
+
elapsedMs?: number,
|
|
585
|
+
): Record<string, unknown> {
|
|
586
|
+
const traceparent = headers.traceparent;
|
|
587
|
+
const traceId = traceparent ? traceIdFromTraceparent(traceparent) : undefined;
|
|
588
|
+
return {
|
|
589
|
+
endpoint,
|
|
590
|
+
method: 'GET',
|
|
591
|
+
correlationId: headers[BOOKING_CORRELATION_HEADER],
|
|
592
|
+
traceparent,
|
|
593
|
+
...(traceId ? { traceId } : {}),
|
|
594
|
+
requestAttemptId,
|
|
595
|
+
attemptNumber,
|
|
596
|
+
maxRetries: BOOKING_GET_MAX_RETRIES,
|
|
597
|
+
requestUrlPath: endpoint,
|
|
598
|
+
requestMode: 'cors',
|
|
599
|
+
requestCache: 'default',
|
|
600
|
+
requestCredentials: 'same-origin',
|
|
601
|
+
requestUrlSearchLength: (() => {
|
|
602
|
+
try {
|
|
603
|
+
return new URL(url, window.location.href).search.length;
|
|
604
|
+
} catch {
|
|
605
|
+
return 0;
|
|
606
|
+
}
|
|
607
|
+
})(),
|
|
608
|
+
availabilityQueryShape: getAvailabilityQueryShape(url),
|
|
609
|
+
visibilityState: typeof document !== 'undefined' ? document.visibilityState : undefined,
|
|
610
|
+
pageAgeMs: typeof performance !== 'undefined' ? Math.round(performance.now()) : undefined,
|
|
611
|
+
connection: getConnectionDiagnostics(),
|
|
612
|
+
...getBrowserDiagnostics(),
|
|
613
|
+
...(elapsedMs != null ? { elapsedMs } : {}),
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
363
617
|
async function fetchBookingGetWithRetry(
|
|
364
618
|
url: string,
|
|
365
619
|
extra?: Pick<RequestInit, 'cache' | 'signal'>,
|
|
366
620
|
): Promise<Response> {
|
|
367
621
|
let lastErr: unknown;
|
|
622
|
+
const endpoint = getEndpointFromUrl(url);
|
|
368
623
|
for (let attempt = 0; attempt <= BOOKING_GET_MAX_RETRIES; attempt++) {
|
|
369
624
|
if (attempt > 0) {
|
|
370
625
|
await sleep(250 * attempt);
|
|
371
626
|
}
|
|
627
|
+
const requestAttemptId = newBookingAttemptId();
|
|
628
|
+
const headers: Record<string, string> = {
|
|
629
|
+
...getAuthHeaders(),
|
|
630
|
+
[BOOKING_ATTEMPT_HEADER]: requestAttemptId,
|
|
631
|
+
};
|
|
632
|
+
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
633
|
+
if (isBookingCriticalEndpoint(endpoint)) {
|
|
634
|
+
reportBookingClientTelemetryEvent('BOOKING_DIALOG_REQUEST_STARTED', {
|
|
635
|
+
...bookingRequestTelemetryContext(url, endpoint, headers, requestAttemptId, attempt + 1),
|
|
636
|
+
});
|
|
637
|
+
}
|
|
372
638
|
try {
|
|
373
639
|
const res = await fetch(url, {
|
|
374
640
|
...extra,
|
|
375
641
|
method: 'GET',
|
|
376
|
-
headers
|
|
642
|
+
headers,
|
|
377
643
|
});
|
|
644
|
+
const elapsedMs = Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt);
|
|
645
|
+
if (
|
|
646
|
+
elapsedMs >= SLOW_REQUEST_THRESHOLD_MS ||
|
|
647
|
+
(isBookingCriticalEndpoint(endpoint) && Math.random() < SUCCESS_TIMING_SAMPLE_RATE)
|
|
648
|
+
) {
|
|
649
|
+
reportBookingClientTelemetryEvent('BOOKING_DIALOG_REQUEST_TIMING', {
|
|
650
|
+
...bookingRequestTelemetryContext(url, endpoint, headers, requestAttemptId, attempt + 1, elapsedMs),
|
|
651
|
+
httpStatus: res.status,
|
|
652
|
+
ok: res.ok,
|
|
653
|
+
resourceTiming: latestResourceTiming(url),
|
|
654
|
+
});
|
|
655
|
+
}
|
|
378
656
|
if (res.ok) {
|
|
379
657
|
return res;
|
|
380
658
|
}
|
|
@@ -388,8 +666,36 @@ async function fetchBookingGetWithRetry(
|
|
|
388
666
|
} catch (e) {
|
|
389
667
|
lastErr = e;
|
|
390
668
|
if (attempt < BOOKING_GET_MAX_RETRIES) {
|
|
669
|
+
const elapsedMs = Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt);
|
|
670
|
+
reportBookingClientTelemetryEvent('BOOKING_DIALOG_REQUEST_RETRYING', {
|
|
671
|
+
...bookingRequestTelemetryContext(url, endpoint, headers, requestAttemptId, attempt + 1, elapsedMs),
|
|
672
|
+
errorName: e instanceof Error ? e.name : undefined,
|
|
673
|
+
errorMessage: e instanceof Error ? e.message : String(e),
|
|
674
|
+
requestSignalAborted: Boolean(extra?.signal?.aborted),
|
|
675
|
+
});
|
|
391
676
|
continue;
|
|
392
677
|
}
|
|
678
|
+
const elapsedMs = Math.round((typeof performance !== 'undefined' ? performance.now() : Date.now()) - startedAt);
|
|
679
|
+
const resourceTiming = latestResourceTiming(url);
|
|
680
|
+
const probeResults = await collectNetworkFailureProbeResults(
|
|
681
|
+
endpoint,
|
|
682
|
+
headers[BOOKING_CORRELATION_HEADER],
|
|
683
|
+
headers.traceparent
|
|
684
|
+
);
|
|
685
|
+
reportBookingClientTelemetryEvent('BOOKING_DIALOG_NETWORK_DIAGNOSTIC', {
|
|
686
|
+
...bookingRequestTelemetryContext(url, endpoint, headers, requestAttemptId, attempt + 1, elapsedMs),
|
|
687
|
+
errorName: e instanceof Error ? e.name : undefined,
|
|
688
|
+
errorMessage: e instanceof Error ? e.message : String(e),
|
|
689
|
+
errorCause: e instanceof Error && e.cause ? String(e.cause) : undefined,
|
|
690
|
+
requestSignalAborted: Boolean(extra?.signal?.aborted),
|
|
691
|
+
abortReason:
|
|
692
|
+
extra?.signal && 'reason' in extra.signal
|
|
693
|
+
? String(extra.signal.reason ?? '')
|
|
694
|
+
: undefined,
|
|
695
|
+
resourceTiming,
|
|
696
|
+
resourceTimingStatus: resourceTimingStatus(resourceTiming),
|
|
697
|
+
probeResults,
|
|
698
|
+
});
|
|
393
699
|
throw e;
|
|
394
700
|
}
|
|
395
701
|
}
|
|
@@ -1678,6 +1984,7 @@ export interface CreatePaymentIntentRequest {
|
|
|
1678
1984
|
customerEmail?: string;
|
|
1679
1985
|
customerFirstName?: string;
|
|
1680
1986
|
customerLastName?: string;
|
|
1987
|
+
customerPhoneNumber?: string;
|
|
1681
1988
|
currency?: string;
|
|
1682
1989
|
reservationReference?: string;
|
|
1683
1990
|
travelerHotel?: string;
|
|
@@ -1781,6 +2088,7 @@ export interface ConfirmFreeBookingRequest {
|
|
|
1781
2088
|
customerEmail?: string;
|
|
1782
2089
|
customerFirstName?: string;
|
|
1783
2090
|
customerLastName?: string;
|
|
2091
|
+
customerPhoneNumber?: string;
|
|
1784
2092
|
currency?: string;
|
|
1785
2093
|
travelerHotel?: string;
|
|
1786
2094
|
pickupLocationId?: string;
|
|
@@ -1860,6 +2168,7 @@ export interface ConfirmBookingWithoutPaymentRequest {
|
|
|
1860
2168
|
customerEmail?: string;
|
|
1861
2169
|
customerFirstName?: string;
|
|
1862
2170
|
customerLastName?: string;
|
|
2171
|
+
customerPhoneNumber?: string;
|
|
1863
2172
|
currency?: string;
|
|
1864
2173
|
travelerHotel?: string;
|
|
1865
2174
|
pickupLocationId?: string;
|
package/src/lib/booking-types.ts
CHANGED
|
@@ -20,6 +20,11 @@ export interface ProductConfig {
|
|
|
20
20
|
optionIds: string[];
|
|
21
21
|
/** STANDARD or PRIVATE_SHUTTLE - affects availability date format. Default STANDARD. */
|
|
22
22
|
productType?: 'STANDARD' | 'PRIVATE_SHUTTLE';
|
|
23
|
+
/**
|
|
24
|
+
* TicketBooth dependent add-on product ids this primary product supports at checkout.
|
|
25
|
+
* Used by photo-first flows to filter the shuttle picker.
|
|
26
|
+
*/
|
|
27
|
+
dependentAddOnProductIds?: string[];
|
|
23
28
|
display: ProductDisplayConfig;
|
|
24
29
|
}
|
|
25
30
|
|
|
@@ -16,12 +16,16 @@ export interface DapDescriptionResult {
|
|
|
16
16
|
paragraphs: string[];
|
|
17
17
|
review?: { text: string; name: string };
|
|
18
18
|
sections: { title: string; content: DapSectionContent }[];
|
|
19
|
+
learnMoreUrl?: string;
|
|
20
|
+
learnMoreLabel?: string;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
interface DapDescriptionData {
|
|
22
24
|
paragraphs: string[];
|
|
23
25
|
review?: { text: string; name: string };
|
|
24
26
|
sections: { title: string; content: DapSectionContent }[];
|
|
27
|
+
learnMoreUrl?: string;
|
|
28
|
+
learnMoreLabel?: string;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
const EN: Record<PhotoDapSlug, DapDescriptionData> = {
|
|
@@ -46,5 +50,7 @@ export function getDapDescription(
|
|
|
46
50
|
paragraphs: data.paragraphs ?? [],
|
|
47
51
|
review: data.review,
|
|
48
52
|
sections: data.sections ?? [],
|
|
53
|
+
learnMoreUrl: data.learnMoreUrl,
|
|
54
|
+
learnMoreLabel: data.learnMoreLabel,
|
|
49
55
|
};
|
|
50
56
|
}
|
|
@@ -305,6 +305,12 @@ export async function getDependentAddOnAvailability(
|
|
|
305
305
|
}
|
|
306
306
|
const payload = await parseJsonSafely(res);
|
|
307
307
|
if (!res.ok) {
|
|
308
|
+
if (res.status === 404) {
|
|
309
|
+
throw new Error('We could not find that shuttle booking reference. Check the code and try again.');
|
|
310
|
+
}
|
|
311
|
+
if (res.status === 403) {
|
|
312
|
+
throw new Error('That last name does not match the shuttle booking. Check the spelling and try again.');
|
|
313
|
+
}
|
|
308
314
|
throw new Error(messageFromPayload(payload, `Could not load times (HTTP ${res.status})`));
|
|
309
315
|
}
|
|
310
316
|
const data = payload as { data?: Record<string, unknown> } | null;
|