keystone-design-bootstrap 1.0.85 → 1.0.87

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 (57) hide show
  1. package/dist/company-information-C1pP-SvU.d.ts +50 -0
  2. package/dist/config-C_XBZixg.d.ts +21 -0
  3. package/dist/consumer-BWjQawiO.d.ts +48 -0
  4. package/dist/design_system/portal/index.d.ts +52 -0
  5. package/dist/design_system/portal/index.js +3113 -0
  6. package/dist/design_system/portal/index.js.map +1 -0
  7. package/dist/design_system/sections/index.d.ts +2 -1
  8. package/dist/index.d.ts +5 -24
  9. package/dist/index.js +156 -37
  10. package/dist/index.js.map +1 -1
  11. package/dist/lib/consumer-session.d.ts +16 -0
  12. package/dist/lib/consumer-session.js +85 -0
  13. package/dist/lib/consumer-session.js.map +1 -0
  14. package/dist/lib/cta-urls.d.ts +34 -0
  15. package/dist/lib/cta-urls.js +33 -0
  16. package/dist/lib/cta-urls.js.map +1 -0
  17. package/dist/lib/server-api.d.ts +2 -1
  18. package/dist/lib/server-api.js +1 -1
  19. package/dist/lib/server-api.js.map +1 -1
  20. package/dist/next/contexts/form-definitions.d.ts +17 -0
  21. package/dist/next/contexts/form-definitions.js +21 -0
  22. package/dist/next/contexts/form-definitions.js.map +1 -0
  23. package/dist/next/gallery/design-gallery.d.ts +103 -0
  24. package/dist/next/gallery/design-gallery.js +19301 -0
  25. package/dist/next/gallery/design-gallery.js.map +1 -0
  26. package/dist/next/layouts/root-layout.d.ts +55 -0
  27. package/dist/next/layouts/root-layout.js +19713 -0
  28. package/dist/next/layouts/root-layout.js.map +1 -0
  29. package/dist/next/legal/privacy-policy.d.ts +7 -0
  30. package/dist/next/legal/privacy-policy.js +18949 -0
  31. package/dist/next/legal/privacy-policy.js.map +1 -0
  32. package/dist/next/legal/terms-of-service.d.ts +7 -0
  33. package/dist/next/legal/terms-of-service.js +18949 -0
  34. package/dist/next/legal/terms-of-service.js.map +1 -0
  35. package/dist/next/providers/ssr-provider.d.ts +12 -0
  36. package/dist/next/providers/ssr-provider.js +12 -0
  37. package/dist/next/providers/ssr-provider.js.map +1 -0
  38. package/dist/next/routes/chat.d.ts +26 -0
  39. package/dist/next/routes/chat.js +160 -0
  40. package/dist/next/routes/chat.js.map +1 -0
  41. package/dist/next/routes/consumer-auth.d.ts +33 -0
  42. package/dist/next/routes/consumer-auth.js +254 -0
  43. package/dist/next/routes/consumer-auth.js.map +1 -0
  44. package/dist/next/routes/form.d.ts +37 -0
  45. package/dist/next/routes/form.js +97 -0
  46. package/dist/next/routes/form.js.map +1 -0
  47. package/dist/package-IU_GpDA0.d.ts +74 -0
  48. package/dist/types/index.d.ts +6 -68
  49. package/package.json +30 -28
  50. package/src/design_system/chat/useRealtimeReplyOrchestrator.ts +127 -0
  51. package/src/design_system/components/ChatWidget.tsx +58 -37
  52. package/src/design_system/portal/MessageComposer.tsx +53 -1
  53. package/src/design_system/portal/PortalPage.tsx +3 -2
  54. package/src/lib/server-api.ts +2 -1
  55. package/src/next/routes/chat.ts +57 -1
  56. package/src/types/rails-actioncable.d.ts +16 -0
  57. package/dist/package-DeHKpQp7.d.ts +0 -121
@@ -1,7 +1,8 @@
1
1
  'use client';
2
2
 
3
- import React, { useState, useTransition, useRef, useEffect } from 'react';
3
+ import React, { useState, useTransition, useRef, useEffect, useCallback } from 'react';
4
4
  import { useRouter } from 'next/navigation';
5
+ import { useRealtimeReplyOrchestrator, type RealtimeSubscriptionData } from '../chat/useRealtimeReplyOrchestrator';
5
6
 
6
7
  export function MessageComposer({ contactId }: { contactId: number }) {
7
8
  const [body, setBody] = useState('');
@@ -10,6 +11,42 @@ export function MessageComposer({ contactId }: { contactId: number }) {
10
11
  const textareaRef = useRef<HTMLTextAreaElement>(null);
11
12
  const router = useRouter();
12
13
 
14
+ const hasAgentReplyWithBody = useCallback(async () => {
15
+ const response = await fetch(`/api/chat/?contact_id=${encodeURIComponent(contactId)}`);
16
+ if (!response.ok) return false;
17
+ const result = await response.json();
18
+ const messages = (result?.data || []) as Array<{ sender_type?: string; body?: string | null }>;
19
+ const latest = messages[messages.length - 1];
20
+ return (
21
+ latest?.sender_type === 'agent' &&
22
+ latest?.body != null &&
23
+ String(latest.body).trim() !== ''
24
+ );
25
+ }, [contactId]);
26
+
27
+ const fetchRealtimeData = useCallback(async (): Promise<RealtimeSubscriptionData | null> => {
28
+ const response = await fetch(`/api/chat/?action=realtime_token&contact_id=${encodeURIComponent(contactId)}`);
29
+ if (!response.ok) return null;
30
+ const result = await response.json();
31
+ const token = result?.data?.token as string | undefined;
32
+ const streamContactId = result?.data?.contact_id as number | undefined;
33
+ const cableUrl = result?.data?.cable_url as string | undefined;
34
+ if (!token || !streamContactId || !cableUrl) return null;
35
+ return { token, contact_id: streamContactId, cable_url: cableUrl };
36
+ }, [contactId]);
37
+
38
+ const {
39
+ beginReplyWait,
40
+ ensureRealtimeSubscription,
41
+ } = useRealtimeReplyOrchestrator({
42
+ debugLabel: 'MessageComposer',
43
+ fetchRealtimeData,
44
+ loadLatestHasReply: hasAgentReplyWithBody,
45
+ onReplyResolved: () => {
46
+ router.refresh();
47
+ },
48
+ });
49
+
13
50
  // Auto-resize textarea
14
51
  useEffect(() => {
15
52
  const el = textareaRef.current;
@@ -18,6 +55,10 @@ export function MessageComposer({ contactId }: { contactId: number }) {
18
55
  el.style.height = `${Math.min(el.scrollHeight, 120)}px`;
19
56
  }, [body]);
20
57
 
58
+ useEffect(() => {
59
+ void ensureRealtimeSubscription();
60
+ }, [ensureRealtimeSubscription]);
61
+
21
62
  function submit() {
22
63
  if (!body.trim() || isPending) return;
23
64
  setError(null);
@@ -34,6 +75,17 @@ export function MessageComposer({ contactId }: { contactId: number }) {
34
75
  } else {
35
76
  setBody('');
36
77
  router.refresh();
78
+ const hasBackgroundReplyJob = Boolean(result?.data?.job_id);
79
+ if (hasBackgroundReplyJob) {
80
+ const realtimeFromSend = result?.data?.realtime_token && result?.data?.contact_id && result?.data?.cable_url
81
+ ? {
82
+ token: result.data.realtime_token,
83
+ contact_id: result.data.contact_id,
84
+ cable_url: result.data.cable_url,
85
+ }
86
+ : null;
87
+ beginReplyWait({ realtimeData: realtimeFromSend });
88
+ }
37
89
  }
38
90
  } catch {
39
91
  setError('Failed to send message.');
@@ -72,12 +72,13 @@ export interface PortalPageProps {
72
72
  /** HEAD-request the booking URL and inspect framing headers to decide whether to embed in an iframe. */
73
73
  async function checkIframeAllowed(url: string): Promise<boolean> {
74
74
  try {
75
- const res = await fetch(url, {
75
+ const fetchOptions: RequestInit & { next?: { revalidate?: number } } = {
76
76
  method: 'HEAD',
77
77
  redirect: 'follow',
78
78
  // Cache for 1 hour — booking platforms rarely change their embedding policy.
79
79
  next: { revalidate: 3600 },
80
- });
80
+ };
81
+ const res = await fetch(url, fetchOptions);
81
82
 
82
83
  const xfo = res.headers.get('x-frame-options')?.toUpperCase();
83
84
  if (xfo === 'DENY' || xfo === 'SAMEORIGIN') return false;
@@ -165,7 +165,8 @@ export async function getJobPosting(slug: string) {
165
165
  }
166
166
 
167
167
  export async function getSocialPosts() {
168
- return serverFetch('/public/social_posts', defaultOptions);
168
+ // Social payloads can exceed Next's data-cache item size; avoid noisy build warnings.
169
+ return serverFetch('/public/social_posts', { cache: 'no-store' });
169
170
  }
170
171
 
171
172
  /** Packages (bundles of service items). */
@@ -25,9 +25,22 @@ import { clientContextHeaders } from './proxy-headers';
25
25
 
26
26
  const API_URL = process.env.API_URL || 'http://localhost:3000/api/v1';
27
27
  const API_KEY = process.env.API_KEY || '';
28
+ const CONSUMER_TOKEN_COOKIE = 'ks_consumer_token';
28
29
 
29
30
  type JsonResponder = (body: unknown, init?: ResponseInit) => Response;
30
31
 
32
+ function cookieValue(cookieHeader: string | null, key: string): string | null {
33
+ if (!cookieHeader) return null;
34
+ const token = cookieHeader
35
+ .split(';')
36
+ .map((part) => part.trim())
37
+ .find((part) => part.startsWith(`${key}=`))
38
+ ?.split('=')
39
+ .slice(1)
40
+ .join('=');
41
+ return token ? decodeURIComponent(token) : null;
42
+ }
43
+
31
44
  export function createChatRouteHandlers(deps?: { NextResponse?: { json: JsonResponder } }) {
32
45
  const json: JsonResponder = deps?.NextResponse?.json ?? ((body, init) => Response.json(body, init));
33
46
 
@@ -65,7 +78,9 @@ export function createChatRouteHandlers(deps?: { NextResponse?: { json: JsonResp
65
78
  { status: response.status }
66
79
  );
67
80
  }
68
- return json({ success: true, data: data.data });
81
+ const responseData = data.data || {};
82
+ const cableUrl = API_URL.replace(/\/api\/v1\/?$/, '').replace(/^http/, 'ws') + '/cable';
83
+ return json({ success: true, data: { ...responseData, cable_url: cableUrl } });
69
84
  } catch (error) {
70
85
  console.error('Chat message error:', error);
71
86
  return json({ success: false, error: 'Network error. Please try again later.' }, { status: 500 });
@@ -76,9 +91,50 @@ export function createChatRouteHandlers(deps?: { NextResponse?: { json: JsonResp
76
91
  GET: async (request: Request): Promise<Response> => {
77
92
  try {
78
93
  const { searchParams } = new URL(request.url);
94
+ const action = searchParams.get('action');
79
95
  const identifier = searchParams.get('identifier');
80
96
  const contactId = searchParams.get('contact_id');
81
97
 
98
+ if (action === 'realtime_token') {
99
+ if (!identifier && !contactId) {
100
+ return json({ success: false, error: 'identifier or contact_id is required for realtime token.' }, { status: 400 });
101
+ }
102
+
103
+ const response = contactId
104
+ ? await (async () => {
105
+ const consumerToken = cookieValue(request.headers.get('cookie'), CONSUMER_TOKEN_COOKIE);
106
+ if (!consumerToken) {
107
+ return new Response(JSON.stringify({ error: 'Unauthorized' }), {
108
+ status: 401,
109
+ headers: { 'Content-Type': 'application/json' },
110
+ });
111
+ }
112
+ return fetch(
113
+ `${API_URL}/consumer/me/contacts/${encodeURIComponent(contactId)}/realtime_token`,
114
+ {
115
+ headers: {
116
+ Authorization: `Bearer ${consumerToken}`,
117
+ ...(API_KEY ? { 'X-API-Key': API_KEY } : {}),
118
+ },
119
+ }
120
+ );
121
+ })()
122
+ : await fetch(
123
+ `${API_URL}/public/messages/realtime_token?identifier=${encodeURIComponent(identifier!)}`,
124
+ { headers: { 'X-API-Key': API_KEY } }
125
+ );
126
+ const data = await response.json();
127
+ if (!response.ok) {
128
+ return json(
129
+ { success: false, error: data.error || 'Failed to create realtime token.' },
130
+ { status: response.status }
131
+ );
132
+ }
133
+
134
+ const cableUrl = API_URL.replace(/\/api\/v1\/?$/, '').replace(/^http/, 'ws') + '/cable';
135
+ return json({ success: true, data: { ...(data.data || {}), cable_url: cableUrl } });
136
+ }
137
+
82
138
  if (!identifier && !contactId) {
83
139
  return json({ success: false, error: 'identifier or contact_id is required.' }, { status: 400 });
84
140
  }
@@ -0,0 +1,16 @@
1
+ declare module '@rails/actioncable' {
2
+ export function createConsumer(url?: string): {
3
+ subscriptions: {
4
+ create(
5
+ channel: Record<string, string>,
6
+ callbacks: {
7
+ connected?: () => void;
8
+ disconnected?: () => void;
9
+ rejected?: () => void;
10
+ received?: (data: { type?: string }) => void | Promise<void>;
11
+ }
12
+ ): { unsubscribe: () => void };
13
+ };
14
+ disconnect: () => void;
15
+ };
16
+ }
@@ -1,121 +0,0 @@
1
- import { P as PhotoAttachment, a as Photo } from './photos-CmBdWiuZ.js';
2
-
3
- /** Nested under `service_items[].offers` and `packages[].offers` in public API. */
4
- interface OfferPublic {
5
- id: number;
6
- name: string;
7
- description: string | null;
8
- value_terms: string | null;
9
- active?: boolean;
10
- expires_at?: string | null;
11
- expired?: boolean;
12
- photo_attachments?: PhotoAttachment[];
13
- }
14
-
15
- interface Service {
16
- id: number;
17
- name: string;
18
- slug: string;
19
- description_markdown: string;
20
- summary?: string;
21
- pricing_info?: string;
22
- features_markdown?: string;
23
- featured: boolean;
24
- sort_order: number;
25
- photo_attachments?: PhotoAttachment[];
26
- service_items?: ServiceItem[];
27
- created_at: string;
28
- updated_at: string;
29
- }
30
- interface ServiceItem {
31
- id: number;
32
- name: string;
33
- slug: string;
34
- summary?: string | null;
35
- description_markdown?: string | null;
36
- pricing_info?: string | null;
37
- price_cents?: number | null;
38
- duration_minutes?: number | null;
39
- sort_order: number;
40
- service_id?: number;
41
- photo_attachments?: PhotoAttachment[];
42
- offers?: OfferPublic[];
43
- }
44
- interface ServiceParams {
45
- featured?: boolean;
46
- q?: string;
47
- page?: number;
48
- per_page?: number;
49
- }
50
- type ServiceResponse = Service[];
51
-
52
- interface CompanyInformation {
53
- id: number;
54
- company_name: string;
55
- tagline: string;
56
- mission_statement_markdown?: string;
57
- about_text_markdown?: string;
58
- description_markdown?: string;
59
- values_markdown?: string;
60
- stats_markdown?: string;
61
- founded_year?: number;
62
- logo_photo?: Photo;
63
- favicon_url?: string;
64
- facebook_url?: string;
65
- instagram_url?: string;
66
- tiktok_url?: string;
67
- linkedin_url?: string;
68
- twitter_url?: string;
69
- youtube_url?: string;
70
- pinterest_url?: string;
71
- google_my_business_url?: string;
72
- yelp_url?: string;
73
- tripadvisor_url?: string;
74
- google_reviews_url?: string;
75
- primary_phone?: string;
76
- primary_email?: string;
77
- primary_address_line_1?: string;
78
- primary_address_line_2?: string;
79
- primary_city?: string;
80
- primary_state?: string;
81
- primary_zip_code?: string;
82
- primary_country?: string;
83
- support_email?: string;
84
- sales_email?: string;
85
- business_hours?: string;
86
- external_management_url?: string;
87
- /** Member portal URL for this account. When set, used as the primary CTA instead of external_management_url. */
88
- portal_url?: string | null;
89
- terms_of_service_markdown?: string;
90
- privacy_policy_markdown?: string;
91
- created_at: string;
92
- updated_at: string;
93
- photo_attachments?: PhotoAttachment[];
94
- account_status?: string;
95
- chat_enabled?: boolean;
96
- }
97
- type CompanyInformationResponse = CompanyInformation | null;
98
-
99
- interface PackageItem {
100
- quantity: number;
101
- service_item?: {
102
- id: number;
103
- name: string;
104
- slug: string;
105
- summary?: string | null;
106
- };
107
- }
108
- interface Package {
109
- id: number;
110
- name: string;
111
- slug: string;
112
- summary?: string | null;
113
- description_markdown?: string | null;
114
- pricing_info?: string | null;
115
- price_cents?: number | null;
116
- photo_attachments?: PhotoAttachment[];
117
- package_items?: PackageItem[];
118
- offers?: OfferPublic[];
119
- }
120
-
121
- export type { CompanyInformation as C, OfferPublic as O, Package as P, Service as S, CompanyInformationResponse as a, PackageItem as b, ServiceItem as c, ServiceParams as d, ServiceResponse as e };