@ticketboothapp/booking 0.1.13 → 0.1.15

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ticketboothapp/booking",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "private": false,
5
5
  "sideEffects": false,
6
6
  "publishConfig": {
@@ -0,0 +1,7 @@
1
+ <svg
2
+ width="24" height="24"
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ viewBox="0 0 24 24"
5
+ >
6
+ <path d="M12 4C12.5523 4 13 4.44772 13 5V19C13 19.5523 12.5523 20 12 20C11.4477 20 11 19.5523 11 19V5C11 4.44772 11.4477 4 12 4Z" fill="currentColor"/>
7
+ </svg>
@@ -1,36 +1,48 @@
1
1
  'use client';
2
2
 
3
- import { createContext, useContext, useRef, useCallback, type ReactNode } from 'react';
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useRef,
7
+ useCallback,
8
+ type ReactNode,
9
+ } from 'react';
10
+ import type { Availability, PricingConfig, PrecomputedPricesByCategory } from '@/lib/booking-api';
4
11
 
12
+ /** Cache TTL in milliseconds (5 minutes). Entries older than this are considered stale. */
5
13
  export const AVAILABILITIES_CACHE_TTL_MS = 5 * 60 * 1000;
6
14
 
7
- export type AvailabilityLike = Record<string, unknown>;
8
- export type PricingConfigLike = Record<string, unknown>;
9
- export type PrecomputedPricesByCategoryLike = Record<string, unknown>;
10
-
11
15
  export interface CachedAvailabilitiesData {
12
16
  fetchedRanges: Array<{ start: Date; end: Date }>;
13
- availabilities: AvailabilityLike[];
14
- pricingConfig: PricingConfigLike | null;
15
- precomputedPricesByOption: Record<string, PrecomputedPricesByCategoryLike> | null;
16
- precomputedPrices?: PrecomputedPricesByCategoryLike | null;
17
+ availabilities: Availability[];
18
+ pricingConfig: PricingConfig | null;
19
+ precomputedPricesByOption: Record<string, PrecomputedPricesByCategory> | null;
20
+ /** Private Shuttle: shared precomputed prices (category -> currency -> price). */
21
+ precomputedPrices?: PrecomputedPricesByCategory | null;
22
+ /** Private Shuttle: resource price by currency. */
17
23
  resourcePriceByCurrency?: Record<string, number> | null;
24
+ /** Private Shuttle: resource price by option (optionId -> currency -> price). */
18
25
  resourcePriceByOption?: Record<string, Record<string, number>> | null;
26
+ /** Timestamp when this entry was cached (for TTL / stale-while-revalidate). */
19
27
  cachedAt: number;
20
28
  }
21
29
 
22
30
  interface AvailabilitiesCacheContextValue {
31
+ /** Get cached data for a product+options+promo. Returns undefined if not cached. */
23
32
  get: (cacheKey: string) => CachedAvailabilitiesData | undefined;
33
+ /** Check if cached entry is stale (older than TTL). Use for stale-while-revalidate. */
24
34
  isStale: (cached: CachedAvailabilitiesData) => boolean;
35
+ /** Replace or set cache entry. */
25
36
  set: (cacheKey: string, data: CachedAvailabilitiesData) => void;
37
+ /** Merge new availabilities into existing cache (or create new entry). */
26
38
  merge: (
27
39
  cacheKey: string,
28
40
  update: {
29
41
  fetchedRanges?: Array<{ start: Date; end: Date }>;
30
- availabilities?: AvailabilityLike[];
31
- pricingConfig?: PricingConfigLike | null;
32
- precomputedPricesByOption?: Record<string, PrecomputedPricesByCategoryLike> | null;
33
- precomputedPrices?: PrecomputedPricesByCategoryLike | null;
42
+ availabilities?: Availability[];
43
+ pricingConfig?: PricingConfig | null;
44
+ precomputedPricesByOption?: Record<string, PrecomputedPricesByCategory> | null;
45
+ precomputedPrices?: PrecomputedPricesByCategory | null;
34
46
  resourcePriceByCurrency?: Record<string, number> | null;
35
47
  resourcePriceByOption?: Record<string, Record<string, number>> | null;
36
48
  }
@@ -41,9 +53,10 @@ const AvailabilitiesCacheContext = createContext<AvailabilitiesCacheContextValue
41
53
 
42
54
  export function useAvailabilitiesCache() {
43
55
  const ctx = useContext(AvailabilitiesCacheContext);
44
- return ctx;
56
+ return ctx; // May be null if not wrapped
45
57
  }
46
58
 
59
+ /** Build cache key from product + options + promo + optional partner pricing profile. */
47
60
  export function buildAvailabilitiesCacheKey(
48
61
  productId: string,
49
62
  optionIdsKey: string,
@@ -57,7 +70,9 @@ export function buildAvailabilitiesCacheKey(
57
70
  export function AvailabilitiesCacheProvider({ children }: { children: ReactNode }) {
58
71
  const cacheRef = useRef<Map<string, CachedAvailabilitiesData>>(new Map());
59
72
 
60
- const get = useCallback((cacheKey: string) => cacheRef.current.get(cacheKey), []);
73
+ const get = useCallback((cacheKey: string) => {
74
+ return cacheRef.current.get(cacheKey);
75
+ }, []);
61
76
 
62
77
  const isStale = useCallback((cached: CachedAvailabilitiesData) => {
63
78
  const age = Date.now() - (cached.cachedAt ?? 0);
@@ -73,10 +88,10 @@ export function AvailabilitiesCacheProvider({ children }: { children: ReactNode
73
88
  cacheKey: string,
74
89
  update: {
75
90
  fetchedRanges?: Array<{ start: Date; end: Date }>;
76
- availabilities?: AvailabilityLike[];
77
- pricingConfig?: PricingConfigLike | null;
78
- precomputedPricesByOption?: Record<string, PrecomputedPricesByCategoryLike> | null;
79
- precomputedPrices?: PrecomputedPricesByCategoryLike | null;
91
+ availabilities?: Availability[];
92
+ pricingConfig?: PricingConfig | null;
93
+ precomputedPricesByOption?: Record<string, PrecomputedPricesByCategory> | null;
94
+ precomputedPrices?: PrecomputedPricesByCategory | null;
80
95
  resourcePriceByCurrency?: Record<string, number> | null;
81
96
  resourcePriceByOption?: Record<string, Record<string, number>> | null;
82
97
  }
@@ -91,10 +106,8 @@ export function AvailabilitiesCacheProvider({ children }: { children: ReactNode
91
106
  ? update.precomputedPricesByOption
92
107
  : existing?.precomputedPricesByOption ?? null,
93
108
  precomputedPrices: update.precomputedPrices !== undefined ? update.precomputedPrices : existing?.precomputedPrices ?? null,
94
- resourcePriceByCurrency:
95
- update.resourcePriceByCurrency !== undefined ? update.resourcePriceByCurrency : existing?.resourcePriceByCurrency ?? null,
96
- resourcePriceByOption:
97
- update.resourcePriceByOption !== undefined ? update.resourcePriceByOption : existing?.resourcePriceByOption ?? null,
109
+ resourcePriceByCurrency: update.resourcePriceByCurrency !== undefined ? update.resourcePriceByCurrency : existing?.resourcePriceByCurrency ?? null,
110
+ resourcePriceByOption: update.resourcePriceByOption !== undefined ? update.resourcePriceByOption : existing?.resourcePriceByOption ?? null,
98
111
  cachedAt: Date.now(),
99
112
  };
100
113
  cacheRef.current.set(cacheKey, next);
@@ -103,5 +116,10 @@ export function AvailabilitiesCacheProvider({ children }: { children: ReactNode
103
116
  );
104
117
 
105
118
  const value: AvailabilitiesCacheContextValue = { get, set, merge, isStale };
106
- return <AvailabilitiesCacheContext.Provider value={value}>{children}</AvailabilitiesCacheContext.Provider>;
119
+
120
+ return (
121
+ <AvailabilitiesCacheContext.Provider value={value}>
122
+ {children}
123
+ </AvailabilitiesCacheContext.Provider>
124
+ );
107
125
  }
@@ -1,21 +1,35 @@
1
1
  'use client';
2
2
 
3
- import { createContext, useContext, type ReactNode } from 'react';
3
+ import { createContext, useContext, ReactNode } from 'react';
4
4
 
5
5
  /** Host app / embedding context (standalone site, provider-dashboard, etc.). */
6
6
  export type BookingAppMode = 'standalone' | 'provider-dashboard' | (string & Record<never, never>);
7
7
 
8
- /** Viewer role for the booking app. */
8
+ /**
9
+ * Viewer role for the booking app. Drives pricing display and (in future) other feature levels.
10
+ * - public: customer-facing (e.g. standalone site); simplified pricing (dynamic increases rolled into base).
11
+ * - reseller: same as public for pricing; reserved for future reseller-specific behaviour.
12
+ * - admin: full detail (e.g. provider dashboard); full price breakdown, no roll-up.
13
+ */
9
14
  export type ViewerRole = 'public' | 'reseller' | 'admin';
10
15
 
11
16
  /** Feature flags and permissions for the booking app in this context. */
12
17
  export interface BookingAppPermissions {
18
+ /** When true, the price breakdown hover tooltip is shown on itinerary line items. */
13
19
  canViewPriceBreakdown?: boolean;
20
+ /**
21
+ * Viewer role. For pricing: public and reseller use simplified view; admin uses full detail.
22
+ * Use this for future feature gating (e.g. reseller vs public vs admin).
23
+ */
14
24
  viewerRole?: ViewerRole;
15
- /** @deprecated Prefer viewerRole. */
25
+ /**
26
+ * @deprecated Prefer viewerRole. When viewerRole is set it takes precedence.
27
+ * How much detail to show in the price breakdown tooltip.
28
+ */
16
29
  priceBreakdownDetail?: 'full' | 'simplified';
17
30
  }
18
31
 
32
+ /** Params for showing the manage-booking UI (e.g. in a dialog when embedded in provider-dashboard). */
19
33
  export interface ManageParams {
20
34
  ref?: string;
21
35
  reservationRef?: string;
@@ -25,11 +39,17 @@ export interface ManageParams {
25
39
  export interface BookingAppContextValue {
26
40
  mode: BookingAppMode;
27
41
  permissions: BookingAppPermissions;
42
+ /** True when pricing should show simplified view (dynamic increases rolled into base). Derived from viewerRole (public/reseller) or priceBreakdownDetail. */
28
43
  isSimplifiedPricingView: boolean;
44
+ /** When set (e.g. by host), used for Google Maps instead of NEXT_PUBLIC_GOOGLE_MAPS_API_KEY. Fixes prod when embedded code doesn't get build-time env. */
29
45
  googleMapsApiKey?: string;
46
+ /** When set (e.g. provider-dashboard), called instead of redirecting to /manage after free booking or when payment success should show manage UI in-host. */
30
47
  onShowManage?: (params: ManageParams) => void;
48
+ /** When set, used as Stripe return_url so payment success lands on the host (e.g. dashboard) instead of /manage. */
31
49
  getSuccessUrl?: (params: { reservationRef: string; lastName: string; focusDate?: string }) => string;
50
+ /** When false, the language selector is hidden. Host-controlled; default true. */
32
51
  showLanguageSelector: boolean;
52
+ /** Partner portal / embedded host: skip auto-scroll after choosing a calendar date. */
33
53
  suppressCalendarDateScroll?: boolean;
34
54
  }
35
55
 
@@ -52,15 +72,26 @@ const DEFAULT_STANDALONE: BookingAppContextValue = {
52
72
 
53
73
  export interface BookingAppProviderProps {
54
74
  children: ReactNode;
75
+ /** Which app or page is hosting the booking UI. */
55
76
  mode?: BookingAppMode;
77
+ /** Permissions for this context (e.g. show price breakdown only for staff). */
56
78
  permissions?: Partial<BookingAppPermissions>;
79
+ /** Google Maps API key from the host (use when embedding so the key from the host build is used). */
57
80
  googleMapsApiKey?: string;
81
+ /** When set (e.g. provider-dashboard), called instead of redirecting to /manage; host can show manage UI in a dialog. */
58
82
  onShowManage?: (params: ManageParams) => void;
83
+ /** When set, used as Stripe return_url so payment success lands on the host. */
59
84
  getSuccessUrl?: (params: { reservationRef: string; lastName: string; focusDate?: string }) => string;
85
+ /** When false, the language selector is hidden. Default true. */
60
86
  showLanguageSelector?: boolean;
87
+ /** When true, calendar date selection does not auto-scroll the page/dialog. */
61
88
  suppressCalendarDateScroll?: boolean;
62
89
  }
63
90
 
91
+ /**
92
+ * Provides the current "mode" (which app is using the booking app) and permissions.
93
+ * Wrap the booking app at the host level: standalone page or BookingWidget when embedded.
94
+ */
64
95
  export function BookingAppProvider({
65
96
  children,
66
97
  mode = 'standalone',
@@ -83,11 +114,21 @@ export function BookingAppProvider({
83
114
  suppressCalendarDateScroll,
84
115
  };
85
116
 
86
- return <BookingAppContext.Provider value={value}>{children}</BookingAppContext.Provider>;
117
+ return (
118
+ <BookingAppContext.Provider value={value}>
119
+ {children}
120
+ </BookingAppContext.Provider>
121
+ );
87
122
  }
88
123
 
124
+ /**
125
+ * Hook to access booking app context (mode and permissions).
126
+ * Use this to gate features like the price breakdown tooltip based on who is using the app.
127
+ */
89
128
  export function useBookingApp(): BookingAppContextValue {
90
129
  const context = useContext(BookingAppContext);
91
- if (context === undefined) return DEFAULT_STANDALONE;
130
+ if (context === undefined) {
131
+ return DEFAULT_STANDALONE;
132
+ }
92
133
  return context;
93
134
  }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
- import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
4
- import { getCompany, type Company } from '@/lib/api';
3
+ import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
4
+ import { getCompany, type Company } from '@/lib/booking-api';
5
5
  import { ENV } from '@/lib/env';
6
6
 
7
7
  interface CompanyContextType {
@@ -14,41 +14,56 @@ const CompanyContext = createContext<CompanyContextType | undefined>(undefined);
14
14
 
15
15
  interface CompanyProviderProps {
16
16
  children: ReactNode;
17
- /** Optional company ID. When provided (e.g. from provider dashboard), use instead of ENV.COMPANY_ID. */
18
- companyId?: string;
19
17
  }
20
18
 
21
- export function CompanyProvider({ children, companyId: companyIdProp }: CompanyProviderProps) {
19
+ /**
20
+ * Provider component that fetches company data once and makes it available via context
21
+ */
22
+ export function CompanyProvider({ children }: CompanyProviderProps) {
22
23
  const [company, setCompany] = useState<Company | null>(null);
23
24
  const [loading, setLoading] = useState(true);
24
25
  const [error, setError] = useState<string | null>(null);
25
- const companyId = companyIdProp ?? ENV.COMPANY_ID;
26
26
 
27
27
  useEffect(() => {
28
28
  async function fetchCompany() {
29
29
  try {
30
- const companyData = await getCompany(companyId);
30
+ const companyData = await getCompany(ENV.COMPANY_ID);
31
31
  setCompany(companyData);
32
32
  setError(null);
33
33
  } catch (err) {
34
34
  console.warn('Failed to fetch company data, using defaults:', err);
35
35
  setError(err instanceof Error ? err.message : 'Failed to fetch company');
36
+ // Don't set company to null - let components use defaults
36
37
  } finally {
37
38
  setLoading(false);
38
39
  }
39
40
  }
41
+
40
42
  fetchCompany();
41
- }, [companyId]);
43
+ }, []);
42
44
 
43
- return <CompanyContext.Provider value={{ company, loading, error }}>{children}</CompanyContext.Provider>;
45
+ return (
46
+ <CompanyContext.Provider value={{ company, loading, error }}>
47
+ {children}
48
+ </CompanyContext.Provider>
49
+ );
44
50
  }
45
51
 
52
+ /**
53
+ * Hook to access company context
54
+ * Returns company data, loading state, and error
55
+ */
46
56
  export function useCompany() {
47
57
  const context = useContext(CompanyContext);
48
- if (context === undefined) throw new Error('useCompany must be used within a CompanyProvider');
58
+ if (context === undefined) {
59
+ throw new Error('useCompany must be used within a CompanyProvider');
60
+ }
49
61
  return context;
50
62
  }
51
63
 
64
+ /**
65
+ * Hook to get company timezone with fallback
66
+ */
52
67
  export function useCompanyTimezone(): string {
53
68
  const { company } = useCompany();
54
69
  return company?.settings?.timezone || 'America/Edmonton';
package/src/index.ts CHANGED
@@ -55,7 +55,6 @@ export {
55
55
 
56
56
  export {
57
57
  setPartnerPortalBookingJwtGetter,
58
- getPartnerPortalBookingJwt,
59
58
  } from './lib/booking-api';
60
59
 
61
60
  export {