@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.
Files changed (32) hide show
  1. package/package.json +1 -1
  2. package/src/components/booking/BookingDialog.module.css +9 -0
  3. package/src/components/booking/BookingProductGrid.module.css +11 -0
  4. package/src/components/booking/BookingProductGrid.tsx +54 -28
  5. package/src/components/booking/CancellationPolicySelector.tsx +4 -1
  6. package/src/components/booking/CheckoutForm.module.css +108 -3
  7. package/src/components/booking/CheckoutForm.tsx +13 -1
  8. package/src/components/booking/CheckoutOptionalPhoneFields.tsx +58 -0
  9. package/src/components/booking/DapTourDescription.tsx +9 -7
  10. package/src/components/booking/DependentAddOnBookingDialog.tsx +42 -7
  11. package/src/components/booking/NewBookingFlow.tsx +137 -55
  12. package/src/components/booking/PrivateShuttleBookingFlow.module.css +7 -0
  13. package/src/components/booking/PrivateShuttleBookingFlow.tsx +21 -0
  14. package/src/components/booking/booking-flow-types.ts +2 -0
  15. package/src/components/booking/booking-flow-ui.ts +2 -0
  16. package/src/components/booking/booking-flow.css +72 -4
  17. package/src/data/dap-descriptions/session-couples-families-friends.en.json +0 -3
  18. package/src/data/dap-descriptions/session-elopements.en.json +12 -12
  19. package/src/data/dap-descriptions/session-proposals.en.json +6 -9
  20. package/src/data/products-config.json +20 -0
  21. package/src/lib/booking/checkout-contact.ts +8 -0
  22. package/src/lib/booking/i18n/messages/en.json +6 -0
  23. package/src/lib/booking/i18n/messages/fr.json +6 -0
  24. package/src/lib/booking/phone.ts +18 -0
  25. package/src/lib/booking-api.ts +310 -1
  26. package/src/lib/booking-types.ts +5 -0
  27. package/src/lib/dap-descriptions.ts +6 -0
  28. package/src/lib/dependent-add-on-api.ts +6 -0
  29. package/src/lib/photo-dap-config.ts +92 -15
  30. package/src/providers/dependent-add-on-dialog-provider.tsx +6 -0
  31. package/src/runtime/types.ts +2 -0
  32. package/src/strings/en.json +6 -6
@@ -41,41 +41,48 @@ const COUPLES_SESSION_PRODUCT_OPTIONS = [
41
41
  const ELOPEMENTS_SESSION_PRODUCT_OPTIONS = [
42
42
  {
43
43
  dependentAddOnProductOptionId: 'dapo_XTeJt09NyNfX',
44
- label: '30 minutes',
45
- photosLabel: '25 photos',
46
- startingAtLabel: 'Starting at $799',
44
+ name: 'Alpine Vows',
45
+ label: '1 hr',
46
+ photosLabel: '60+ photos',
47
+ startingAtLabel: '$999',
47
48
  },
48
49
  {
49
50
  dependentAddOnProductOptionId: 'dapo_Y8AxYt6Tjaam',
50
- label: '60 minutes',
51
- photosLabel: '50 photos',
52
- startingAtLabel: 'Starting at $999',
51
+ name: 'The Summit Story',
52
+ label: '4hrs',
53
+ photosLabel: '100+ photos',
54
+ startingAtLabel: '$2799',
53
55
  },
54
56
  {
55
57
  dependentAddOnProductOptionId: 'dapo_YdmxIiQPxEJg',
56
- label: '90 minutes',
57
- photosLabel: '75 photos',
58
- startingAtLabel: 'Starting at $1199',
58
+ name: 'The Grand Adventure',
59
+ label: '8hrs',
60
+ photosLabel: '100+ photos',
61
+ startingAtLabel: '$3999',
59
62
  },
60
63
  ] as const;
61
64
 
62
65
  const PROPOSALS_SESSION_PRODUCT_OPTIONS = [
63
66
  {
64
67
  dependentAddOnProductOptionId: 'dapo_SMRxjfDpIvwU',
68
+ name: 'Essentials',
65
69
  label: '30 minutes',
66
- photosLabel: '25 photos',
70
+ photosLabel: '30 photos',
67
71
  startingAtLabel: 'Starting at $799',
68
72
  },
69
73
  {
70
74
  dependentAddOnProductOptionId: 'dapo_FyMbFgrBU4L8',
75
+ name: 'Celebration',
71
76
  label: '60 minutes',
72
- photosLabel: '50 photos',
77
+ photosLabel: '60 photos',
73
78
  startingAtLabel: 'Starting at $999',
79
+ mostPopular: true,
74
80
  },
75
81
  {
76
82
  dependentAddOnProductOptionId: 'dapo_EOohfF0g2i1D',
83
+ name: 'Immersive',
77
84
  label: '90 minutes',
78
- photosLabel: '75 photos',
85
+ photosLabel: '90 photos',
79
86
  startingAtLabel: 'Starting at $1199',
80
87
  },
81
88
  ] as const;
@@ -90,16 +97,78 @@ export type PhotoDapSlug = (typeof PHOTO_DAP_SLUGS)[number];
90
97
 
91
98
  export type PhotoDapProductOption = {
92
99
  dependentAddOnProductOptionId: string;
93
- /** First line on the tile (e.g. duration). */
100
+ /** Package name (e.g. Essentials); when set, `label` is shown as the duration subheading. */
101
+ name?: string;
102
+ /** Duration subheading when `name` is set; otherwise the tile title. */
94
103
  label: string;
95
- /** Second line (e.g. edited photo count). */
104
+ /** Photo count line (e.g. 30 photos). */
96
105
  photosLabel?: string;
97
- /** Third line (e.g. "Starting at $399"). */
106
+ /** Price line (e.g. Starting at $799). */
98
107
  startingAtLabel?: string;
108
+ mostPopular?: boolean;
109
+ };
110
+
111
+ export function defaultPhotoDapProductOptionId(
112
+ options: readonly PhotoDapProductOption[]
113
+ ): string | undefined {
114
+ const popular = options.find((option) => option.mostPopular);
115
+ return popular?.dependentAddOnProductOptionId ?? options[0]?.dependentAddOnProductOptionId;
116
+ }
117
+
118
+ export type PhotoDapUpsellCardLines = {
119
+ startingPrice: string;
120
+ duration: string;
121
+ quantity: string;
99
122
  };
100
123
 
124
+ function parsePriceFromStartingAtLabel(label: string | undefined): number | null {
125
+ if (!label) return null;
126
+ const match = label.match(/\$([\d,]+)/);
127
+ if (!match) return null;
128
+ return Number.parseInt(match[1].replace(/,/g, ''), 10);
129
+ }
130
+
131
+ function formatPhotosSummary(options: readonly PhotoDapProductOption[]): string {
132
+ const parts = options
133
+ .map((option) => option.photosLabel?.replace(/\s*photos?\s*$/i, '').trim())
134
+ .filter((part): part is string => Boolean(part));
135
+ if (parts.length === 0) return '';
136
+ return `${parts.join(' / ')} photos`;
137
+ }
138
+
139
+ const UPSELL_STARTING_PRICE_BY_SLUG: Record<
140
+ PhotoDapSlug,
141
+ (minPrice: number) => string
142
+ > = {
143
+ 'session-couples-families-friends': (price) => `Sessions from $${price}`,
144
+ 'session-elopements': (price) => `Starting at $${price}`,
145
+ 'session-proposals': (price) => `Beginning at $${price}`,
146
+ };
147
+
148
+ /** Lines for manage-booking upsell cards — derived from the same options as the booking picker. */
149
+ export function getPhotoDapUpsellCardLines(slug: PhotoDapSlug): PhotoDapUpsellCardLines | null {
150
+ const catalog = getPhotoDapCatalog(slug);
151
+ const options = catalog?.productOptions;
152
+ if (!options?.length) return null;
153
+
154
+ const prices = options
155
+ .map((option) => parsePriceFromStartingAtLabel(option.startingAtLabel))
156
+ .filter((price): price is number => price != null);
157
+ const minPrice = prices.length > 0 ? Math.min(...prices) : null;
158
+
159
+ return {
160
+ startingPrice:
161
+ minPrice != null
162
+ ? UPSELL_STARTING_PRICE_BY_SLUG[slug](minPrice)
163
+ : (options[0]?.startingAtLabel ?? ''),
164
+ duration: options.map((option) => option.label).join(' / '),
165
+ quantity: formatPhotosSummary(options),
166
+ };
167
+ }
168
+
101
169
  export type PhotoDapCatalog = {
102
170
  dependentAddOnProductId: string;
171
+ receiptDisplayTitle: string;
103
172
  /** When set, that option is fixed (no picker). Env `NEXT_PUBLIC_DAP_PHOTO_SESSION_COUPLES_OPTION_ID` forces this for couples. */
104
173
  dependentAddOnProductOptionId?: string;
105
174
  /** When multiple entries and no fixed option id, the dialog shows a session-length control */
@@ -111,6 +180,8 @@ export type PhotoDapCatalog = {
111
180
  * Should match TicketBooth dependent add-on product config; availability API may override when present.
112
181
  */
113
182
  cancellationDaysBeforeSession: number;
183
+ learnMoreUrl?: string;
184
+ learnMoreLabel?: string;
114
185
  };
115
186
 
116
187
  /** Image sets for the dependent add-on dialog collage (photos only; no video). */
@@ -163,6 +234,7 @@ export function getPhotoDapCatalog(slug: PhotoDapSlug): PhotoDapCatalog | null {
163
234
  if (forcedOptionId) {
164
235
  return {
165
236
  dependentAddOnProductId: productId,
237
+ receiptDisplayTitle: 'Friends, family, and couples photography session',
166
238
  dependentAddOnProductOptionId: forcedOptionId,
167
239
  collageImageIds,
168
240
  cancellationDaysBeforeSession: DEFAULT_PHOTO_DAP_CANCELLATION_DAYS_BEFORE_SESSION,
@@ -170,6 +242,7 @@ export function getPhotoDapCatalog(slug: PhotoDapSlug): PhotoDapCatalog | null {
170
242
  }
171
243
  return {
172
244
  dependentAddOnProductId: productId,
245
+ receiptDisplayTitle: 'Friends, family, and couples photography session',
173
246
  productOptions: [...COUPLES_SESSION_PRODUCT_OPTIONS],
174
247
  collageImageIds,
175
248
  cancellationDaysBeforeSession: DEFAULT_PHOTO_DAP_CANCELLATION_DAYS_BEFORE_SESSION,
@@ -187,6 +260,7 @@ export function getPhotoDapCatalog(slug: PhotoDapSlug): PhotoDapCatalog | null {
187
260
  if (forcedOptionId) {
188
261
  return {
189
262
  dependentAddOnProductId: productId,
263
+ receiptDisplayTitle: 'Elopement photography session',
190
264
  dependentAddOnProductOptionId: forcedOptionId,
191
265
  collageImageIds,
192
266
  cancellationDaysBeforeSession: DEFAULT_PHOTO_DAP_CANCELLATION_DAYS_BEFORE_SESSION,
@@ -194,6 +268,7 @@ export function getPhotoDapCatalog(slug: PhotoDapSlug): PhotoDapCatalog | null {
194
268
  }
195
269
  return {
196
270
  dependentAddOnProductId: productId,
271
+ receiptDisplayTitle: 'Elopement photography session',
197
272
  productOptions: [...ELOPEMENTS_SESSION_PRODUCT_OPTIONS],
198
273
  collageImageIds,
199
274
  cancellationDaysBeforeSession: DEFAULT_PHOTO_DAP_CANCELLATION_DAYS_BEFORE_SESSION,
@@ -211,6 +286,7 @@ export function getPhotoDapCatalog(slug: PhotoDapSlug): PhotoDapCatalog | null {
211
286
  if (forcedOptionId) {
212
287
  return {
213
288
  dependentAddOnProductId: productId,
289
+ receiptDisplayTitle: 'Proposal photography session',
214
290
  dependentAddOnProductOptionId: forcedOptionId,
215
291
  collageImageIds,
216
292
  cancellationDaysBeforeSession: DEFAULT_PHOTO_DAP_CANCELLATION_DAYS_BEFORE_SESSION,
@@ -218,6 +294,7 @@ export function getPhotoDapCatalog(slug: PhotoDapSlug): PhotoDapCatalog | null {
218
294
  }
219
295
  return {
220
296
  dependentAddOnProductId: productId,
297
+ receiptDisplayTitle: 'Proposal photography session',
221
298
  productOptions: [...PROPOSALS_SESSION_PRODUCT_OPTIONS],
222
299
  collageImageIds,
223
300
  cancellationDaysBeforeSession: DEFAULT_PHOTO_DAP_CANCELLATION_DAYS_BEFORE_SESSION,
@@ -13,14 +13,18 @@ import type { PhotoDapSlug } from '../lib/photo-dap-config';
13
13
 
14
14
  export type DependentAddOnProductOptionChoice = {
15
15
  dependentAddOnProductOptionId: string;
16
+ name?: string;
16
17
  label: string;
17
18
  photosLabel?: string;
18
19
  startingAtLabel?: string;
20
+ mostPopular?: boolean;
19
21
  };
20
22
 
21
23
  export type DependentAddOnDialogOpenPayload = {
22
24
  /** Card title shown in dialog header */
23
25
  productDisplayTitle: string;
26
+ /** Customer-facing line label for receipts/payment summaries. */
27
+ receiptDisplayTitle?: string;
24
28
  dependentAddOnProductId: string;
25
29
  /** Fixed catalog option (no picker) */
26
30
  dependentAddOnProductOptionId?: string;
@@ -35,6 +39,8 @@ export type DependentAddOnDialogOpenPayload = {
35
39
  collageImageIds?: string[];
36
40
  /** Loads expandable copy from dap-descriptions */
37
41
  dapDescriptionSlug?: PhotoDapSlug;
42
+ learnMoreUrl?: string;
43
+ learnMoreLabel?: string;
38
44
  /**
39
45
  * From DAP catalog / TicketBooth product — days before the photo session for full-refund cancellation.
40
46
  * Availability API may override when it returns the same field.
@@ -53,6 +53,8 @@ export interface BookingRuntimeSlots {
53
53
  ImageModal: BookingSlotComponent;
54
54
  /** Optional override; package supplies a full default terms component when omitted. */
55
55
  TermsContent?: BookingSlotComponent;
56
+ /** Optional phone input with country selector (e.g. Via Via `PhoneInputWithCountry`). */
57
+ PhoneInput?: BookingSlotComponent;
56
58
  PlusIcon: ComponentType<SVGProps<SVGSVGElement>>;
57
59
  MinusIcon: ComponentType<SVGProps<SVGSVGElement>>;
58
60
  /** Via Via `ButtonHoverColor` enum from `@/components/button`. */
@@ -16,7 +16,7 @@
16
16
  },
17
17
 
18
18
  "partnerPortal": {
19
- "pageTitle": "Partner booking",
19
+ "pageTitle": "Partner Booking Portal",
20
20
  "tabBook": "Book",
21
21
  "tabBookings": "Your bookings",
22
22
  "tabLivePickups": "Live pickups",
@@ -49,7 +49,7 @@
49
49
  "bookTabAttributionConfirmLabelNoAgent": "I confirm the partner is correct.",
50
50
  "bookSuccessToast": "Booking made under partner {{partner}} with agent {{agent}}.",
51
51
  "bookSuccessToastNoAgent": "Booking made under partner {{partner}}.",
52
- "signInTitle": "Partner sign-in",
52
+ "signInTitle": "Partner Booking Portal sign-in",
53
53
  "signInDescription": "Use the email on file for your organization (partner contact, account email, or an agent payout email). We will email you a one-time code — no password to remember.",
54
54
  "signInEmailLabel": "Email",
55
55
  "signInSelectEmailPlaceholder": "Choose an email",
@@ -73,7 +73,7 @@
73
73
  "sessionLoading": "Restoring your session…",
74
74
  "bookingsLoading": "Loading your bookings…",
75
75
  "bookingsLoadError": "We could not load your bookings. Try again in a moment.",
76
- "bookingsEmpty": "No partner portal bookings yet. When you complete a booking while signed in, it will appear here.",
76
+ "bookingsEmpty": "No partner booking portal bookings yet. When you complete a booking while signed in, it will appear here.",
77
77
  "livePickupsEmptyToday": "No tour pickups scheduled for today.",
78
78
  "livePickupsGuestPageLink": "Open guest live pickups page",
79
79
  "bookingsColReference": "Reference",
@@ -191,9 +191,9 @@
191
191
  "orgAddAgentDuplicateName": "An agent with this name already exists. Choose a different name.",
192
192
  "orgNone": "—",
193
193
  "agentAddNewOption": "Add an agent…",
194
- "partnerLoginPageTitle": "Partner sign-in",
195
- "partnerLoginPageDescription": "Enter the email on file for your partner account. We will send you a one-time code to complete sign-in.",
196
- "backToPortal": "Back to partner portal"
194
+ "partnerLoginPageTitle": "Partner Booking Portal sign-in",
195
+ "partnerLoginPageDescription": "Enter the email on file for your partner booking account. We will send you a one-time code to complete sign-in.",
196
+ "backToPortal": "Back to partner booking portal"
197
197
  },
198
198
 
199
199
  "staffPortal": {