shopify-chatbot-widget 0.8.6 β†’ 0.8.9

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.
@@ -1,6 +1,6 @@
1
1
  import React, { useState,useCallback } from 'react';
2
2
  import { Button, Avatar } from 'antd';
3
- import { CloseOutlined,ArrowLeftOutlined } from '@ant-design/icons';
3
+ import { CloseOutlined,ArrowLeftOutlined,ShoppingCartOutlined} from '@ant-design/icons';
4
4
  import '../../Chatbot.css';
5
5
  import { useChatbotMessage } from '../../../hooks/useSessionMessages';
6
6
  import axios from 'axios';
@@ -14,7 +14,7 @@ import type { ChatMessage } from '../../../types/chatbot';
14
14
  import { getCookie } from '../../../utils/cookies';
15
15
  import useChatSocket from '../../../hooks/useWebsocke';
16
16
  import Notification from '../Notification';
17
- import CartWidget from '../Messages/CartWidget';
17
+ // import CartWidget from '../Messages/CartWidget';
18
18
  import { useCart } from '../../../hooks/useCart';
19
19
 
20
20
  type MessengerProps = {
@@ -24,10 +24,11 @@ type MessengerProps = {
24
24
  colorCode: string;
25
25
  newSession: boolean;
26
26
  messagesSuggestions: string[];
27
+ setMessagesSuggestions: React.Dispatch<React.SetStateAction<string[]>>;
27
28
  disableForm: boolean;
28
29
  sessionId:string;
29
30
  setOpen: (v: boolean) => void;
30
- setView: (v: string) => void;
31
+ setView: React.Dispatch<React.SetStateAction<'sessions' | 'create' | 'messenger' | 'cartView'>>;
31
32
  setNewSession: (v: boolean) => void;
32
33
  Opened: boolean;
33
34
  calendly:string;
@@ -52,6 +53,7 @@ const Messenger: React.FC<MessengerProps> = ({
52
53
  userType,
53
54
  setNewSession,
54
55
  messagesSuggestions,
56
+ setMessagesSuggestions,
55
57
  disableForm,
56
58
  sessionId,
57
59
  setOpen,
@@ -168,7 +170,10 @@ const socket = useChatSocket(sessionId, handleSocketData);
168
170
  const sendMessageSuggestion = async (message: string): Promise<void> => {
169
171
  if (!message.trim()) return;
170
172
 
171
- setIsSending(true); // πŸ”’ Disable input
173
+ setIsSending(true);
174
+
175
+ // Remove only the clicked suggestion
176
+ setMessagesSuggestions(prev => prev.filter(suggestion => suggestion !== message));
172
177
 
173
178
  const userMsg: ChatMessage = { role: 'user', content: message };
174
179
  setMessages(prev => [...prev, userMsg]);
@@ -187,10 +192,12 @@ const socket = useChatSocket(sessionId, handleSocketData);
187
192
  } catch (error) {
188
193
  console.error("Error sending suggestion:", error);
189
194
  } finally {
190
- setIsSending(false); // πŸ”“ Re-enable input
195
+ setIsSending(false);
191
196
  }
192
197
  };
193
198
 
199
+
200
+
194
201
 
195
202
 
196
203
  async function handleSendImage (image: File, userMessage:string) {
@@ -398,7 +405,7 @@ const socket = useChatSocket(sessionId, handleSocketData);
398
405
  }
399
406
  };
400
407
 
401
- const { items, loading, addToCart, removeFromCart, refresh } = useCart(sessionId);
408
+ const { items, addToCart, removeFromCart } = useCart(sessionId);
402
409
 
403
410
 
404
411
  // replace your old handleToCart:
@@ -407,9 +414,9 @@ const socket = useChatSocket(sessionId, handleSocketData);
407
414
  }
408
415
 
409
416
  const handleToCart = useCallback(
410
- async (variantId: string, name: string, qty = 1) => {
417
+ async (variantId: string, name: string, variantImage: string, qty = 1) => {
411
418
  try {
412
- await addToCart(variantId, name, qty);
419
+ await addToCart(variantId, name, qty, variantImage);
413
420
 
414
421
  const shortName = shorten(name, 20);
415
422
  setNotification(`βœ… ${qty}Γ— ${shortName} added to cart`);
@@ -420,6 +427,7 @@ const socket = useChatSocket(sessionId, handleSocketData);
420
427
  [addToCart]
421
428
  );
422
429
 
430
+
423
431
  const handleRemoveFromCart = useCallback(
424
432
  async (variantId: string, qty = 1) => {
425
433
  await removeFromCart(variantId, qty);
@@ -452,18 +460,59 @@ const socket = useChatSocket(sessionId, handleSocketData);
452
460
  <span className="chatbot-title">{agentName || 'Ai Assistant'}</span>
453
461
 
454
462
  {items.length > 0 && (
455
- <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
456
- <CartWidget
457
- items={items}
458
- loading={loading}
459
- sessionId={sessionId}
460
- onRefresh={refresh}
461
- onRemove={removeFromCart}
462
- colorCode={colorCode}
463
- sendMessage={sendMessageSuggestion}
464
- />
465
- </div>
466
- )}
463
+ <div
464
+ style={{
465
+ position: "relative",
466
+ display: "flex",
467
+ alignItems: "center",
468
+ justifyContent: "center",
469
+ marginLeft: "auto",
470
+ marginRight: 12,
471
+ }}
472
+ >
473
+ <ShoppingCartOutlined
474
+ onClick={() => setView("cartView")}
475
+ style={{
476
+ fontSize: 26,
477
+ cursor: "pointer",
478
+ color: "#333",
479
+ padding: 6,
480
+ borderRadius: "50%",
481
+ transition: "0.2s",
482
+ }}
483
+ onMouseEnter={(e) => {
484
+ (e.currentTarget as HTMLElement).style.background = "rgba(0,0,0,0.05)";
485
+ }}
486
+ onMouseLeave={(e) => {
487
+ (e.currentTarget as HTMLElement).style.background = "transparent";
488
+ }}
489
+ />
490
+
491
+ {/* Counter Badge */}
492
+ <span
493
+ style={{
494
+ position: "absolute",
495
+ top: 2,
496
+ right: 2,
497
+ backgroundColor: colorCode || "#7F28F8",
498
+ color: "white",
499
+ fontSize: 10,
500
+ fontWeight: 600,
501
+ width: 18,
502
+ height: 18,
503
+ borderRadius: "50%",
504
+ display: "flex",
505
+ alignItems: "center",
506
+ justifyContent: "center",
507
+ boxShadow: "0 1px 3px rgba(0,0,0,0.3)",
508
+ }}
509
+ >
510
+ {items.length}
511
+ </span>
512
+ </div>
513
+ )}
514
+
515
+
467
516
 
468
517
 
469
518
  {/* Close Button */}
@@ -46,13 +46,13 @@ const ChatSessions: React.FC<ChatSessionsProps> = ({ chatbotLogo, colorCode, onN
46
46
  (now.getTime() - createdDate.getTime()) / (1000 * 60 * 60 * 24)
47
47
  );
48
48
 
49
- const isOlderThan3Days = diffDays >= 3;
49
+ const isOlderThan5Days = diffDays >= 5;
50
50
 
51
51
  return {
52
52
  id: item.user_session_id,
53
53
  agentName: item.assignee || 'AI Agent',
54
54
  createdAt: item.date,
55
- isBlurred: item.closed || isOlderThan3Days, // βœ… auto-close if 3+ days old
55
+ isBlurred: item.closed || isOlderThan5Days, // βœ… auto-close if 3+ days old
56
56
  agentImage: item.assignee_picture,
57
57
  };
58
58
  });
@@ -5,12 +5,14 @@ export interface CartItem {
5
5
  variantId: string;
6
6
  name: string;
7
7
  quantity: number;
8
+ variantImage: string;
8
9
  }
9
10
 
10
11
  export function useCart(sessionId: string) {
11
12
  const [items, setItems] = useState<CartItem[]>([]);
12
13
  const [loading, setLoading] = useState(false);
13
14
 
15
+ // βœ… Fetch current cart
14
16
  const fetchCart = useCallback(async () => {
15
17
  if (!sessionId) return;
16
18
 
@@ -26,61 +28,66 @@ export function useCart(sessionId: string) {
26
28
  }
27
29
  }, [sessionId]);
28
30
 
29
- const addToCart = async (variantId: string, name: string, qty: number) => {
31
+ // βœ… Add to Cart (includes image)
32
+ const addToCart = async (
33
+ variantId: string,
34
+ name: string,
35
+ qty: number,
36
+ variantImage: string
37
+ ) => {
30
38
  if (!sessionId) return;
31
-
32
- // optimistic update
33
39
 
34
-
35
40
  try {
36
41
  const res = await fetch(`${config.apiUrl}cart/add/`, {
37
- method: "POST",
38
- headers: { "Content-Type": "application/json" },
42
+ method: 'POST',
43
+ headers: { 'Content-Type': 'application/json' },
39
44
  body: JSON.stringify({
40
45
  session_id: sessionId,
41
46
  variantId,
42
47
  name,
43
48
  quantity: qty,
49
+ variantImage, // βœ… include image
44
50
  }),
45
51
  });
46
-
52
+
47
53
  const json = await res.json();
48
54
  if (!res.ok) {
49
- throw new Error(json.error || "Failed to add to cart");
50
- }
51
- else{
52
- setItems(current => {
53
- const found = current.find(i => i.variantId === variantId);
54
- if (found) {
55
- return current.map(i =>
56
- i.variantId === variantId
57
- ? { ...i, quantity: i.quantity + qty }
58
- : i
59
- );
60
- }
61
- return [...current, { variantId, name, quantity: qty }];
62
- });
55
+ throw new Error(json.error || 'Failed to add to cart');
63
56
  }
57
+
58
+ // βœ… Optimistic update
59
+ setItems((current) => {
60
+ const found = current.find((i) => i.variantId === variantId);
61
+ if (found) {
62
+ return current.map((i) =>
63
+ i.variantId === variantId
64
+ ? { ...i, quantity: i.quantity + qty }
65
+ : i
66
+ );
67
+ }
68
+ return [...current, { variantId, name, quantity: qty, variantImage }];
69
+ });
64
70
  } catch (err: any) {
71
+ console.error('Error adding to cart:', err);
65
72
  throw err;
66
73
  }
67
-
68
- await fetchCart(); // reconcile
74
+
75
+ await fetchCart(); // βœ… reconcile with server
69
76
  };
70
-
71
77
 
78
+ // βœ… Remove from Cart
72
79
  const removeFromCart = async (variantId: string, qty: number) => {
73
80
  if (!sessionId) return;
74
81
 
75
82
  // Optimistic update
76
- setItems(current =>
83
+ setItems((current) =>
77
84
  current
78
- .map(i =>
85
+ .map((i) =>
79
86
  i.variantId === variantId
80
87
  ? { ...i, quantity: i.quantity - qty }
81
88
  : i
82
89
  )
83
- .filter(i => i.quantity > 0)
90
+ .filter((i) => i.quantity > 0)
84
91
  );
85
92
 
86
93
  try {
@@ -94,11 +101,13 @@ export function useCart(sessionId: string) {
94
101
  }),
95
102
  });
96
103
  } catch (err) {
104
+ console.error('Error removing from cart:', err);
97
105
  }
98
106
 
99
- await fetchCart(); // reconcile server data
107
+ await fetchCart(); // βœ… reconcile with server
100
108
  };
101
109
 
110
+ // βœ… Fetch cart on mount
102
111
  useEffect(() => {
103
112
  fetchCart();
104
113
  }, [fetchCart]);
@@ -127,6 +127,7 @@ export const useChatbotData = () => {
127
127
  calendly,
128
128
  merchantPhone,
129
129
  messagesSuggestions,
130
+ setMessagesSuggestions,
130
131
  restSuggestions,
131
132
  userType,
132
133
  disbleCheckout,
@@ -0,0 +1,45 @@
1
+ import config from "./config";
2
+
3
+ interface CheckoutResponse {
4
+ checkoutUrl: string;
5
+ }
6
+
7
+ export const useCartAPI = () => {
8
+ const checkOut = async (sessionId: string): Promise<void> => {
9
+ try {
10
+ const companyUsername = localStorage.getItem('company_username');
11
+ if (!companyUsername) throw new Error('No company username found.');
12
+
13
+ const response = await fetch(`${config.apiUrl}checkout/shopify`, {
14
+ method: 'POST',
15
+ headers: { 'Content-Type': 'application/json' },
16
+ body: JSON.stringify({ sessionId, company_username: companyUsername }),
17
+ });
18
+
19
+ if (!response.ok) throw new Error('Failed to initiate checkout.');
20
+
21
+ const data: CheckoutResponse = await response.json();
22
+
23
+ if (data?.checkoutUrl) {
24
+ // Open in parent frame for embedded chatbots
25
+ window.parent?.postMessage(
26
+ {
27
+ type: 'JAWEB_CHAT',
28
+ action: 'navigate',
29
+ target: '_blank',
30
+ url: data.checkoutUrl,
31
+ },
32
+ '*'
33
+ );
34
+ } else {
35
+ alert('No checkout URL returned.');
36
+ }
37
+ } catch (err) {
38
+ console.error('Checkout error:', err);
39
+ }
40
+ };
41
+
42
+ return {
43
+ checkOut,
44
+ };
45
+ };
@@ -1,259 +0,0 @@
1
- import React, { useState,useEffect } from 'react';
2
- import { Button } from 'antd';
3
-
4
- import { useChatbotData } from '../hooks/useChatbotData';
5
- import ChatSessions from './components/Sessions/RenderList';
6
- import CreateSession from './components/Sessions/CreateSession';
7
- import Messenger from './components/Messenger/Messenger';
8
- import './Chatbot.css'
9
- import config from '../hooks/config';
10
- import FingerprintJS from '@fingerprintjs/fingerprintjs';
11
-
12
- const Chatbot: React.FC = () => {
13
- const [open, setOpen] = useState(false);
14
- const [viewMode, setViewMode] = useState<'sessions' | 'create' | 'messenger'>('sessions');
15
- const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
16
- const [HasInfo,setHasInfo]=useState(false)
17
- const [activeAgentName,setActiveAgentName]=useState('')
18
- const [activeAgentImage,setActiveAgentImage]=useState('')
19
- const [newSession,setNewSession]=useState(false)
20
-
21
-
22
- const windowWidth = window.innerWidth;
23
-
24
- const {
25
- chatbotLogo,
26
- colorCode,
27
- subscription,
28
- messagesSuggestions,
29
- isFetchingDetails,
30
- disableForm,
31
- merchantPhone,
32
- userType,
33
- calendly,
34
- disbleCheckout
35
- } = useChatbotData();
36
-
37
- const [visitorData, setVisitorData] = useState<{ visitorId: string; country: string } | null>(null);
38
-
39
-
40
- const handleNewSessionClick = () => {
41
- // If the form is *not* disabled, always go to create
42
- if (!disableForm && !HasInfo) {
43
- setViewMode('create');
44
- return;
45
- }
46
-
47
- else {
48
- const newSessionId = crypto.randomUUID();
49
- setActiveSessionId(newSessionId);
50
- setActiveAgentImage(chatbotLogo);
51
- setActiveAgentName('Ai Agent');
52
- setNewSession(true);
53
- setViewMode('messenger');
54
- }
55
-
56
- // Otherwise (disableForm true && !HasInfo) we simply bail.
57
- };
58
-
59
- useEffect(() => {
60
- const fetchVisitorInfo = async () => {
61
- try {
62
- let visitorId = localStorage.getItem('visitor_id');
63
- let ip = localStorage.getItem('ip_address');
64
- let country = localStorage.getItem('country');
65
-
66
- // βœ… If all cached, safely set and exit
67
- // if (visitorId && ip && country) {
68
- // setVisitorData({ visitorId, country });
69
-
70
- // return;
71
- // }
72
-
73
- // πŸ” Generate fingerprint if needed
74
- if (!visitorId) {
75
- const fp = await FingerprintJS.load();
76
- const result = await fp.get();
77
- visitorId = result.visitorId;
78
- localStorage.setItem('visitor_id', visitorId);
79
- }
80
-
81
- // 🌍 Fetch IP info if missing
82
- if (!ip || !country) {
83
- const res = await fetch('https://ipapi.co/json');
84
- const data = await res.json();
85
- ip = data.ip;
86
- country = data.country_name;
87
-
88
- if (ip) localStorage.setItem('ip_address', ip);
89
- if (country) localStorage.setItem('country', country);
90
- }
91
-
92
- // βœ… Ensure values are non-null before use
93
- if (visitorId && ip && country) {
94
- setVisitorData({ visitorId, country });
95
-
96
- // πŸ“‘ Send to backend
97
- const backendRes = await fetch(`${config.apiUrl}register-ip/`, {
98
- method: 'POST',
99
- headers: {
100
- 'Content-Type': 'application/json',
101
- },
102
- body: JSON.stringify({
103
- ip_address: ip,
104
- country,
105
- visitor_id: visitorId,
106
- }),
107
- });
108
-
109
- const result = await backendRes.json();
110
- setHasInfo(result.has_user_info || false);
111
- } else {
112
- console.warn('Missing IP or country info after fetch');
113
- }
114
- } catch (err) {
115
- console.error('Failed to fetch or send visitor info', err);
116
- }
117
- };
118
-
119
- fetchVisitorInfo();
120
- }, []);
121
-
122
-
123
-
124
-
125
-
126
- const handleSessionSelect = (sessionId: string,agentName:string,agentImage:string) => {
127
- setActiveSessionId(sessionId);
128
- setActiveAgentName(agentName)
129
- setActiveAgentImage(agentImage)
130
- setNewSession(false)
131
- setViewMode('messenger');
132
- };
133
-
134
-
135
- useEffect(() => {
136
- const onMessage = (e: MessageEvent) => {
137
- const msg = (e.data || {}) as any;
138
- if (msg.type !== 'JAWEB_CHAT') return;
139
-
140
- if (msg.action === 'open-ready') {
141
- // Parent has expanded iframe β†’ now we can render full UI
142
- setOpen(true);
143
- } else if (msg.action === 'closed') {
144
- // Parent collapsed iframe β†’ hide full UI
145
- setOpen(false);
146
- }
147
- };
148
- window.addEventListener('message', onMessage);
149
- return () => window.removeEventListener('message', onMessage);
150
- }, []);
151
-
152
- const requestOpen = () => {
153
- // Ask parent to expand iframe
154
- window.parent?.postMessage({ type: 'JAWEB_CHAT', action: 'open' }, '*');
155
- };
156
- const requestClose = () => {
157
- // Ask parent to collapse iframe
158
- window.parent?.postMessage({ type: 'JAWEB_CHAT', action: 'close' }, '*');
159
- };
160
-
161
-
162
-
163
-
164
- return (
165
- <div>
166
- {!isFetchingDetails && subscription ? (
167
- <div
168
- style={{
169
- bottom: windowWidth < 768 ? '20%' : 0,
170
- top: windowWidth < 768 ? '0' : '5%',
171
- height: windowWidth < 768 ? '100%' : '85%',
172
- borderRadius: 10,
173
- width: '100%',
174
- zIndex: 9999,
175
- }}
176
- >
177
- {open && (
178
- <div className="chatbot-box">
179
- {viewMode === 'sessions' && (
180
- <ChatSessions
181
- chatbotLogo={chatbotLogo}
182
- Phone={merchantPhone || ''}
183
- colorCode={colorCode}
184
- visitorId={visitorData?.visitorId || ''}
185
- Opened={open}
186
- // when a child wants to close, talk to parent
187
- setOpen={(v: boolean) => (v ? requestOpen() : requestClose())}
188
- onNewSession={handleNewSessionClick}
189
- onSessionSelect={handleSessionSelect}
190
- />
191
- )}
192
-
193
- {viewMode === 'create' && (
194
- <CreateSession
195
- colorCode={colorCode}
196
- visitorId={visitorData?.visitorId || ''}
197
- country={visitorData?.country || ''}
198
- onBack={() => setViewMode('sessions')}
199
- onCreate={(userData) => {
200
- setActiveSessionId(userData.session_id);
201
- setHasInfo(userData.hasInfo);
202
- setViewMode('messenger');
203
- }}
204
- />
205
- )}
206
-
207
- {viewMode === 'messenger' && activeSessionId && (
208
- <Messenger
209
- sessionId={activeSessionId}
210
- chatbotLogo={activeAgentImage || chatbotLogo}
211
- agentName={activeAgentName || 'Ai Assistant'}
212
- colorCode={colorCode}
213
- messagesSuggestions={messagesSuggestions}
214
- newSession={newSession}
215
- disableForm={disableForm}
216
- calendly={calendly ?? ''}
217
- // children call this to close/open β†’ routed to parent
218
- setOpen={(v: boolean) => (v ? requestOpen() : requestClose())}
219
- setNewSession={setNewSession}
220
- Opened={open}
221
- setView={() => setViewMode('sessions')}
222
- userType={userType}
223
- disbleCheckout={disbleCheckout}
224
- />
225
- )}
226
- </div>
227
- )}
228
- </div>
229
- ) : null}
230
-
231
- <div className="jaweb-chatbot-container">
232
- {!isFetchingDetails && subscription ? (
233
- <Button
234
- type="primary"
235
- shape="circle"
236
- icon={
237
- <img
238
- src="https://jaweb.me/wp-content/uploads/2025/03/download__2_-removebg-preview.png"
239
- alt="Chat Icon"
240
- style={{ width: 30, height: 30 }}
241
- />
242
- }
243
- size="large"
244
- style={{ backgroundColor: colorCode, width: 60, height: 60 }}
245
- className={`chat-launcher ${open ? 'chatbot-open' : ''}`}
246
- // πŸ”‘ Don’t setOpen directly; request parent resize
247
- onClick={() => (open ? requestClose() : requestOpen())}
248
- />
249
- ) : null}
250
- </div>
251
- </div>
252
- );
253
- };
254
-
255
- export default Chatbot;
256
-
257
-
258
-
259
-