keystone-design-bootstrap 1.0.91 → 1.0.92
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/dist/index.js +11 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/design_system/chat/useRealtimeReplyOrchestrator.ts +0 -1
- package/src/design_system/components/ChatWidget.tsx +22 -16
- package/src/design_system/portal/BookIframePanel.tsx +47 -22
- package/src/design_system/portal/PortalPage.tsx +11 -7
package/package.json
CHANGED
|
@@ -74,26 +74,29 @@ export function ChatWidget({
|
|
|
74
74
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
75
75
|
const [inputValue, setInputValue] = useState('');
|
|
76
76
|
const [isLoading, setIsLoading] = useState(false);
|
|
77
|
-
|
|
77
|
+
// Resolve the anonymous session id once, up front. Authenticated visitors
|
|
78
|
+
// (contactId) don't need one. The value is never rendered, so deriving it
|
|
79
|
+
// lazily is hydration-safe and avoids a setState-in-effect cascade.
|
|
80
|
+
const [sessionId] = useState<string>(() => {
|
|
81
|
+
if (contactId) return '';
|
|
82
|
+
if (providedSessionId) return providedSessionId;
|
|
83
|
+
if (typeof window === 'undefined') return '';
|
|
84
|
+
return (
|
|
85
|
+
localStorage.getItem('keystone_chat_session_id') ??
|
|
86
|
+
`session_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`
|
|
87
|
+
);
|
|
88
|
+
});
|
|
78
89
|
const [waitingForReply, setWaitingForReply] = useState(false);
|
|
79
90
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
80
91
|
|
|
81
|
-
//
|
|
92
|
+
// Persist a freshly generated anonymous session id. The write lives in an
|
|
93
|
+
// effect (out of render) and never calls setState, so renders don't cascade.
|
|
82
94
|
useEffect(() => {
|
|
83
|
-
if (contactId) return;
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
} else {
|
|
87
|
-
const stored = localStorage.getItem('keystone_chat_session_id');
|
|
88
|
-
if (stored) {
|
|
89
|
-
setSessionId(stored);
|
|
90
|
-
} else {
|
|
91
|
-
const newId = `session_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
92
|
-
localStorage.setItem('keystone_chat_session_id', newId);
|
|
93
|
-
setSessionId(newId);
|
|
94
|
-
}
|
|
95
|
+
if (contactId || providedSessionId || !sessionId) return;
|
|
96
|
+
if (localStorage.getItem('keystone_chat_session_id') !== sessionId) {
|
|
97
|
+
localStorage.setItem('keystone_chat_session_id', sessionId);
|
|
95
98
|
}
|
|
96
|
-
}, [contactId, providedSessionId]);
|
|
99
|
+
}, [contactId, providedSessionId, sessionId]);
|
|
97
100
|
|
|
98
101
|
const loadMessages = useCallback(async () => {
|
|
99
102
|
// Need either a contactId or an active session before fetching.
|
|
@@ -115,9 +118,12 @@ export function ChatWidget({
|
|
|
115
118
|
return [];
|
|
116
119
|
}, [contactId, sessionId]);
|
|
117
120
|
|
|
118
|
-
// Load message history when opened (either authenticated or session is ready)
|
|
121
|
+
// Load message history when opened (either authenticated or session is ready).
|
|
122
|
+
// loadMessages is async — any setState happens after the fetch resolves, not
|
|
123
|
+
// synchronously during the effect, so it doesn't trigger a render cascade.
|
|
119
124
|
useEffect(() => {
|
|
120
125
|
if (isOpen && (contactId || sessionId)) {
|
|
126
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- async fetch-on-open; state updates after await
|
|
121
127
|
loadMessages();
|
|
122
128
|
}
|
|
123
129
|
}, [isOpen, contactId, sessionId, loadMessages]);
|
|
@@ -5,9 +5,14 @@ import React, { useState, useEffect, useRef } from 'react';
|
|
|
5
5
|
interface BookIframePanelProps {
|
|
6
6
|
bookingHref: string;
|
|
7
7
|
businessName: string;
|
|
8
|
+
/**
|
|
9
|
+
* When false, the blurred preview is shown but the overlay card becomes a
|
|
10
|
+
* sign-in prompt instead of the "Start Booking" / "Open in New Tab" actions.
|
|
11
|
+
*/
|
|
12
|
+
isLoggedIn?: boolean;
|
|
8
13
|
}
|
|
9
14
|
|
|
10
|
-
export function BookIframePanel({ bookingHref, businessName }: BookIframePanelProps) {
|
|
15
|
+
export function BookIframePanel({ bookingHref, businessName, isLoggedIn = true }: BookIframePanelProps) {
|
|
11
16
|
const [hasOpened, setHasOpened] = useState(false);
|
|
12
17
|
const [modalOpen, setModalOpen] = useState(false);
|
|
13
18
|
const [isVisible, setIsVisible] = useState(false);
|
|
@@ -67,31 +72,51 @@ export function BookIframePanel({ bookingHref, businessName }: BookIframePanelPr
|
|
|
67
72
|
|
|
68
73
|
{/* Backdrop blur + message overlay */}
|
|
69
74
|
<div className="absolute inset-0 flex flex-col items-center justify-center px-6 py-8 backdrop-blur-[3.9px] bg-primary/70">
|
|
70
|
-
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
{isLoggedIn ? (
|
|
76
|
+
<div className="w-full max-w-sm rounded-component border border-secondary bg-primary px-6 py-6 text-center shadow-sm">
|
|
77
|
+
<p className="text-xl font-bold text-primary">Booking Instructions</p>
|
|
78
|
+
<p className="mt-5 text-sm text-secondary leading-relaxed">
|
|
79
|
+
We use a separate platform that may ask for an additional sign in or intake details to fully create your profile and collect your appointment information.
|
|
80
|
+
</p>
|
|
81
|
+
<div className="mt-5 flex flex-wrap items-center justify-center gap-3">
|
|
82
|
+
<button
|
|
83
|
+
onClick={openModal}
|
|
84
|
+
className="inline-flex items-center gap-2 rounded-interactive bg-brand-solid px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-brand-solid_hover"
|
|
85
|
+
>
|
|
86
|
+
Start Booking
|
|
87
|
+
</button>
|
|
88
|
+
<a
|
|
89
|
+
href={bookingHref}
|
|
90
|
+
target="_blank"
|
|
91
|
+
rel="noopener noreferrer"
|
|
92
|
+
className="inline-flex items-center gap-1.5 rounded-interactive border border-secondary bg-secondary px-5 py-2.5 text-sm font-semibold text-primary transition-colors hover:bg-secondary_hover"
|
|
93
|
+
>
|
|
94
|
+
Open in New Tab
|
|
95
|
+
<svg className="size-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
96
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
|
97
|
+
</svg>
|
|
98
|
+
</a>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
) : (
|
|
102
|
+
<div className="w-full max-w-sm rounded-component border border-secondary bg-primary px-6 py-6 text-center shadow-sm">
|
|
103
|
+
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-secondary">
|
|
104
|
+
<svg className="size-5 text-quaternary" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
105
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
|
|
106
|
+
</svg>
|
|
107
|
+
</div>
|
|
108
|
+
<p className="text-xl font-bold text-primary">Sign in to book</p>
|
|
109
|
+
<p className="mt-3 text-sm text-secondary leading-relaxed">
|
|
110
|
+
Create a free account or sign in to book your appointment.
|
|
111
|
+
</p>
|
|
76
112
|
<button
|
|
77
|
-
|
|
78
|
-
className="inline-flex items-center gap-2 rounded-interactive bg-brand-solid px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-brand-solid_hover"
|
|
113
|
+
data-open-login-modal
|
|
114
|
+
className="mt-5 inline-flex cursor-pointer items-center gap-2 rounded-interactive bg-brand-solid px-5 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-brand-solid_hover"
|
|
79
115
|
>
|
|
80
|
-
|
|
116
|
+
Sign in
|
|
81
117
|
</button>
|
|
82
|
-
<a
|
|
83
|
-
href={bookingHref}
|
|
84
|
-
target="_blank"
|
|
85
|
-
rel="noopener noreferrer"
|
|
86
|
-
className="inline-flex items-center gap-1.5 rounded-interactive border border-secondary bg-secondary px-5 py-2.5 text-sm font-semibold text-primary transition-colors hover:bg-secondary_hover"
|
|
87
|
-
>
|
|
88
|
-
Open in New Tab
|
|
89
|
-
<svg className="size-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
90
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
|
91
|
-
</svg>
|
|
92
|
-
</a>
|
|
93
118
|
</div>
|
|
94
|
-
|
|
119
|
+
)}
|
|
95
120
|
</div>
|
|
96
121
|
</div>
|
|
97
122
|
|
|
@@ -538,19 +538,23 @@ function BookPanel({
|
|
|
538
538
|
isLoggedIn: boolean;
|
|
539
539
|
businessName: string;
|
|
540
540
|
}) {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
541
|
+
// When the platform supports embedding, always show the blurred preview.
|
|
542
|
+
// The overlay card itself adapts: a sign-in prompt when logged out, the
|
|
543
|
+
// booking instructions + actions when logged in.
|
|
545
544
|
if (bookingAllowsIframe) {
|
|
546
545
|
return (
|
|
547
546
|
<>
|
|
548
|
-
<PortalTabTracker event="InitiateCheckout" tab="booking" />
|
|
549
|
-
<BookIframePanel bookingHref={bookingHref} businessName={businessName} />
|
|
547
|
+
{isLoggedIn && <PortalTabTracker event="InitiateCheckout" tab="booking" />}
|
|
548
|
+
<BookIframePanel bookingHref={bookingHref} businessName={businessName} isLoggedIn={isLoggedIn} />
|
|
550
549
|
</>
|
|
551
550
|
);
|
|
552
551
|
}
|
|
553
552
|
|
|
553
|
+
// No embeddable preview to tease — keep booking gated behind the login wall.
|
|
554
|
+
if (!isLoggedIn) {
|
|
555
|
+
return <LoginWall message="Continue to view booking options." cta="View Booking Options" />;
|
|
556
|
+
}
|
|
557
|
+
|
|
554
558
|
return (
|
|
555
559
|
<>
|
|
556
560
|
<PortalTabTracker event="InitiateCheckout" tab="booking" />
|
|
@@ -738,7 +742,7 @@ export async function PortalPage({
|
|
|
738
742
|
<div className="flex gap-1 overflow-x-auto pb-px scrollbar-none">
|
|
739
743
|
{tabs.map((t) => {
|
|
740
744
|
const isActive = tab === t.id;
|
|
741
|
-
const isGated = !isLoggedIn && (t.id === 'specials' || t.id === 'messages'
|
|
745
|
+
const isGated = !isLoggedIn && (t.id === 'specials' || t.id === 'messages');
|
|
742
746
|
const href = `${portalHref}?tab=${t.id}`;
|
|
743
747
|
const className = `shrink-0 rounded-interactive px-4 py-2 text-sm font-medium transition-colors whitespace-nowrap ${
|
|
744
748
|
isActive ? 'bg-brand-solid text-white' : 'text-secondary hover:bg-secondary hover:text-primary'
|